UPDATE
Here is my answer:
Example:
type UnaryReducer = <S>(state: S) => S
interface ApplyReducer {
<T extends UnaryReducer>(reducer: T): <S,>(state: ReturnType<T> & S) => ReturnType<T> & S;
}
export const applyReducer: ApplyReducer = (reducer) =>
(state) => reducer(state)
interface State { a: number, b: number }
const initialState: State = { a: 0, b: 0 }
const bar = applyReducer(
state => ({ ...state, b: 2, })
)(initialState)
bar // {b: number; } & State
const bar2 = applyReducer(
state => ({ ...state, b: '2', })
)(initialState) // Error: b is not a string
const bar3 = applyReducer(
state => ({ ...state, b: 2, c:'2' })
)(initialState) // Error: Property 'c' is missing in type 'State'
const bar4 = applyReducer(
state => ({ ...state })
)(initialState) // Ok
const bar5 = applyReducer(
state => ({ a: 0, b: 0 }) // Error: you should always return object wich is extended by State
)(initialState)
const bar6 = applyReducer(
state => ({...state, a: 0, b: 0 }) // Ok
)(initialState)
We should define generic parameter directly for arrow function
type UnaryReducer = <S>(state: S) => S
We should somehow bind initialState argument and ReturnType of reducer
interface ApplyReducer {
<T extends UnaryReducer>(reducer: T): <S,>(state: ReturnType<T> & S) => ReturnType<T> & S;
}
It is mean that state argument of reducer (callback) should be always a part of return type.
That's mean, that if you will try to:
state => ({ a:0, b: 2, })
it is not gonna work, but I think there is not sence to do it
UPDATE
I think it is impossible to do. Consider this example:
const foo = applyReducer(
state => ({ ...state, b: state.b + 1 })
)
const result = foo((initialState))
You are able to infer initialState only during foo call. NO way TS will know upfront the argument type during foo declaration.
TS infers arguments from left to right.