Reputation: 95
export const createModel = <
T,
R = {
[propName: string]: <P>(prevState: T, payload: P) => T;
}
>(model: {
state: T;
reducers: R;
effects: (dispatch: {
[K in keyof R]: (
payload: R[K] extends (prevState: T, payload: infer P) => T ? P : never
) => T
}) => Record<string, () => Promise<void>>;
}) => model;
const model = createModel<string[]>({
state: ['foo'],
reducers: {
update(prevState, payload: string[]) {
return [
...prevState,
...payload
]
}
},
effects: dispatch => ({
async request() {
dispatch.update(['bar'])
// (payload: unknown) => string[]
}
})
})
But I got:
TS2322: Type '<P>(prevState: string[], payload: string[]) => string[]'
is not assignable to type '<P>(prevState: string[], payload: P) => string[]'
How can I make the attribute function of dispatch get the correct payload type.
Maybe it will work like Vue.defineComponent()
.
By the way, any articles or books can learn typescript in depth?
Upvotes: 1
Views: 254
Reputation: 328187
Conceptually, your createModel()
should have the following type:
export const createModel = <T, P extends object>(
model:
{
state: T,
reducers: {
[K in keyof P]: (prevState: T, payload: P[K]) => T
},
effects: (dispatch: {
[K in keyof P]: (
payload: P[K]
) => T
}) => Record<string, () => Promise<void>>
}
) => model;
Here, T
corresponds to the type of state
, while P
corresponds to the mapping from keys in reducers
and effects
to payload types. This is very straightforward and clean, but unfortunately the compiler really can't infer P
from call sites:
// T inferred as string[],
// P inferred as object
const badModel = createModel({
state: ["foo"], reducers: {
update(prevState, payload: string[]) {
return [
...prevState,
...payload
]
}
}, effects: dispatch => ({
async request() {
dispatch.update(['bar']) // error!
// ----> ~~~~~~ <-- update not present on object
//
}
})
});
Here the type T
is correctly inferred as string[]
, but the compiler is unable to use reducers
or effects
to infer the keys of P
, and the compiler ends up just falling back to the object
constraint. And so you get errors.
If you don't mind manually specifying type parameters when you call createModel()
, then things will work out:
const model = createModel<string[], { update: string[] }>({
state: ["foo"], reducers: {
update(prevState, payload) {
return [
...prevState,
...payload
]
}
}, effects: dispatch => ({
async request() {
dispatch.update(['bar']) // okay
//
}
})
});
So that's one way to proceed.
But if you don't want to write out redundant information (e.g., a third instance of update
and unnecessarily mentioning string[]
for T
), then you need to care about TypeScript's type parameter and contextual type inference algorithm, and its limitations.
Roughly, the compiler takes a certain small number of inference "passes" where it tries to use information to determine candidates for generic type parameter types (like P
), or for the type of an unannotated callback parameter (like prevState
). It plugs in those candidates and checks again to try to infer more things. But it's very easy for the compiler to run out of inference passes before inferring everything you care about, and it gives up and falls back to some general type like unknown
or whatever type a parameter is constrained to. See microsoft/TypeScript#25826, microsoft/TypeScript#29791, and microsoft/TypeScript#38872 for examples.
For now, this is just a limitation of the language. There is a suggesstion at microsoft/TypeScript#30134 for the compiler to use a more complete unification algorithm for type inference, but who knows if anything like that will ever happen. It's best to just work with the system we have.
The most straightforward way around this problem is to split the inference job into manageable chunks, where each chunk needs just one or two things inferred, and then refactor so that the compiler more or less has to infer in that order. In this case, we can take your single generic function and split it into a curried function where each subsequent call adds in a bit more to the model
:
export const createModel = <T,>(state: T) =>
<R extends Record<keyof R, (prevState: T, payload: any) => T>>(reducers: R) =>
(effects: (dispatch: {
[K in keyof R]: (
payload: R[K] extends (prevState: any, payload: infer P) => any ? P : never
) => T
}) => Record<string, () => Promise<void>>) => ({ state, reducers, effects });
Note that it's not always possible to infer a type like P
from a value of a mapped type that depends on P
but is not P
itself. It is much easier to infer a type like R
from a value of type R
itself. This means we have to calculate our payload types from our reducer types, using conditional type inference similar to your original code. It's uglier, but it helps with inference.
Anyway, the order of operations above is: First we ask for state
and infer T
from it. Then we ask for reducers
and infer R
from it, which has already been constrained to an object type that depends on the already-inferred T
. This should also allow the compiler to contextually infer the type of prevState
. And then we ask for effects
, whose type should already be completely known since T
and R
fix it completely.
Let's try it:
const model = createModel(["foo"])({
update(prevState, payload: string[]) {
return [
...prevState,
...payload
]
}
})(dispatch => ({
async request() {
dispatch.update(['bar'])
}
}));
/* const model: {
state: string[];
reducers: {
update(prevState: string[], payload: string[]): string[];
};
effects: (dispatch: {
update: (payload: string[]) => string[];
}) => Record<string, () => Promise<void>>;
} */
Looks good. The compiler infers T
as string[]
and R
as the correct type for reducers
, and then effects
is also properly typed.
It's up to you whether manually specifying types is more or less annoying than writing complexly-typed curried functions.
Upvotes: 1
Reputation: 7186
You likely want P
to extend T
:
export const createModel = <
T,
R = {
[propName: string]: <P extends T>(prevState: T, payload: P) => T;
// ^^^^^^^^^ add this
}
>(model: {
// ...
}) => model
This way, regardless of the previous state, you force the next state (payload
) to match that type.
Upvotes: 0