Issue
Here, I want to be able to infer api as { login: (param: number) => Promise<void> }
, but I'm getting any.
export function createSlice<
T extends Record<
keyof T,
(state: S, payload?: any) => S | ((api: {
[K in keyof T]: Parameters<T[K]>[1] extends undefined
? { (): void }
: { (payload: Parameters<T[K]>[1]): void }
}) => Promise<void>)
>,
N extends string,
S,
>(
config: T, name: N, initialState: S
) {
return {};
}
interface TestState {
isTest: boolean;
}
const slice = createSlice({
login: (state: TestState, param: number) => async (api) => {
api.login('sdf');
},
}, 'testSlice', { isTest: false });
Is this a fundamental limitation? If so, how can I re-work the API to achieve what I want?
Solution
From all I've seen it isn't generally possible to get the sort of simultaneous inference of both generic type parameters and of callback parameter types from context. While it might be easy enough for a human being to intuit the right order in which such inference would have to happen to be successful (in your case, infer the T
type parameter from the keys and second parameters of the methods in the config
argument, and then infer the parameters for callbacks returned from these methods), the compiler isn't really able to do this. It has an algorithm that works well in a lot of cases, but fails in other cases. There are various issues in GitHub about this sort of thing; for example, see microsoft/TypeScript#44999. Mostly these are closed as design limitations, with a link back to microsoft/TypeScript#30134, a suggestion to implement a full unification algorithm that will probably stay an unimplemented suggestion for a long time (or forever) because it would be a huge undertaking without an obviously high chance of improvement.
For now, this means that you will need to cut the Gordian knot either by manually annotating a callback parameter you would prefer not to annotate, or by manually specifying a generic type parameter you would prefer not to specify. I think the latter is probably the better approach, especially if we refactor so that the type you specify is as terse as possible. Here's one approach:
declare const createSlice: <T extends object>() =>
<N extends string, S>(
config: { [K in keyof T]:
(state: S, payload: T[K]) => S | (
(api: { [P in keyof T]: (payload: T[P]) => void }) => Promise<void>
) },
name: N,
initialState: S
) => void;
This is a curried function that works around the lack of partial type argument inference as requested in microsoft/TypeScript#26242. We're going to have to specify T
manually, but we'd still like the compiler to infer N
and S
. That can't be done in a single generic function, so I've split it up so that you can specify T
and then the returned function will infer N
and S
for you. It is what it is.
I've also refactored T
so that it is just a mapping from key to the second method parameter type directly. This type looks like the relatively simple {login: number}
and not the more complex {login: (s: S, p: number) => S | ...
corresponding to the config
parameter. Instead we start with the simpler T
and map it to the type of config
.
Okay, let's test it:
const slice = createSlice<{
login: number,
loginSuccess: void,
}>()({
loginSuccess: (state) => state,
login: (state, param) => async (api) => {
api.login(123); // <-- okay
api.login('sdf'); // <-- error
api.loginSuccess(); // <-- okay
},
}, 'testSlice', { isTest: false });
Looks good. The result of createSlice<{login: number, loginSuccess: void}>()
is of type
<N extends string, S>(config: {
login: (state: S, payload: number) => S | ((api: {
login: (payload: number) => void;
loginSuccess: (payload: void) => void;
}) => Promise<void>);
loginSuccess: (state: S, payload: void) => S | ((api: {
login: (payload: number) => void;
loginSuccess: (payload: void) => void;
}) => Promise<void>);
}, name: N, initialState: S) => void
And when you call that with the above parameters, the compiler can easily infer that N
is of type "testSlice"
, that S
is of type {isTest: boolean}
, and that the unannotated api
callback parameter is of type {login: (payload: number)=>void; loginSuccess: (payload: void) => void}
.
That's about as close as I can think of to what you want here.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.