Here is the code:
type Args<State, ActionHandlers, Helpers> = {
state: State,
actionHandlers: ActionHandlers,
helpers?: Helpers
}
type GenericActionHandler<State, Payload> = (state: State, payload: Payload) => Partial<State> | void
type GenericDispatch<State, ActionHandlers> = <
Key extends keyof ActionHandlers,
Handler extends ActionHandlers[Key],
Payload = Handler extends GenericActionHandler<State, infer P> ? P : never
>(type: Key, ...payload: Payload extends undefined ? [] : [Payload]) => void
type GenericHelper<State, ActionHandlers, Payload> = (dispatch: GenericDispatch<State, ActionHandlers>, payload: Payload) => Promise<void> | void
type HelpersRemapped<State, ActionHandlers, Helpers> = {
[Key in keyof Helpers]: <
Helper extends Helpers[Key],
Payload = Helper extends GenericHelper<State, ActionHandlers, infer P> ? P : never
>(payload: Payload) => Promise<void> | void
}
const create = <
State extends {},
ActionHandlers extends Record<string, GenericActionHandler<State, any>>,
Helpers extends Record<string, GenericHelper<State, ActionHandlers, any>>
>({ state, actionHandlers, helpers }: Args<State, ActionHandlers, Helpers>) => {
const dispatch: GenericDispatch<State, ActionHandlers> = (type, ...payload) => { console.log(type, [payload]) };
type Remap = HelpersRemapped<State, ActionHandlers, Helpers>;
const helpersRemapped: Partial<Remap> = {}
for (const key in helpers) {
helpersRemapped[key] = (payload) => helpers[key](dispatch, payload);
}
return {
dispatch,
helpers: helpersRemapped as Remap
};
}
let { dispatch, helpers } = create({
state: {
msg: "hi",
num: 123,
flag: true
},
actionHandlers: {
SET_MSG: (state, msg: string) => ({ msg }),
SET_NUM: (state, num: number) => ({ num }),
SET_FLAG: (state, flag: boolean) => ({ flag })
},
helpers: {
setFlagAsync: async (dispatch, flag: boolean) => { // I don't understand what the problem is here :'(
await Promise.resolve();
dispatch("SET_FLAG", flag);
}
}
});
helpers.setFlagAsync(true);
dispatch("SET_MSG", "test");
If you click through to the TS Playground link above the problem is underlined in red squiggle on line 61 when trying to define a helper method on the helpers object.
Type '(dispatch: GenericDispatch<{ msg: string; num: number; flag: boolean; }, Record<string, GenericActionHandler<{ msg: string; num: number; flag: boolean; }, any>>>, flag: boolean) => Promise<...>' is not assignable to type 'GenericHelper<{ msg: string; num: number; flag: boolean; }, { SET_MSG: (state: { msg: string; num: number; flag: boolean; }, msg: string) => { msg: string; }; SET_NUM: (state: { msg: string; num: number; flag: boolean; }, num: number) => { ...; }; SET_FLAG: (state: { ...; }, flag: boolean) => { ...; }; }, any>'.
Types of parameters 'dispatch' and 'dispatch' are incompatible.
Types of parameters 'type' and 'type' are incompatible.
Type 'Key' is not assignable to type '"SET_MSG" | "SET_NUM" | "SET_FLAG"'.
Type 'string' is not assignable to type '"SET_MSG" | "SET_NUM" | "SET_FLAG"'.
Type 'Key' is not assignable to type '"SET_FLAG"'.
Type 'string' is not assignable to type '"SET_FLAG"'.(2322)
If you force the code to run it works fine but TypeScript is not satisfied and will not compile. I've been banging my head on this error for the better part of two days and haven't made any progress. Any help is greatly appreciated!
PS - I know, this is similar to what Redux already does. Sadly I do not have control.
CodePudding user response:
You define actionHandlers
inline within your create
function. As you've written your type definition, we expect the keys of the helpers
to be Key extends keyof ActionHandlers
. TypeScript is casting that specifically as one of the key names you defined in the actionHandlers
, but you are passing "SET_FLAG"
, which is being interpereted as a string
. A string
is too wide a definition to statisfy the extends keyof ActionHandlers
type. In other words, Key extends keyof ActionHandlers
is too narrow to be satisfied by a string
type, so typescript errors. When I've run into cases like this, I always think "can't typescript recognize that the value is indeed a keyof
the object??". I guess not.
In any case, if you define actionHandlers
before feeding it to create
, ans cast it as const
, it solves the problem:
const state = {
msg: "hi",
num: 123,
flag: true
}
const actionHandlers = {
SET_MSG: (stateArg: typeof state, msg: string) => ({ msg }),
SET_NUM: (stateArg: typeof state, num: number) => ({ num }),
SET_FLAG: (stateArg: typeof state, flag: boolean) => ({ flag })
} as const;
// Invoking the create function.
let { dispatch, helpers } = create({
state,
actionHandlers,
helpers: {
setFlagAsync: async (dispatch, flag: boolean) => {
await Promise.resolve();
dispatch("SET_FLAG", flag);
}
}
});
Working typescript playground
Unfortunately when moving the declaration of the argument outside of the function call, we lose the typing on the state
variable, so I had to explicitly type it.
Edit
As I struggling to fall asleep last night, I ended up realizing there's a quicker way from your original code to a solution. Just use as const
inline, like this:
let { dispatch, helpers } = create({
state: {
msg: "hi",
num: 123,
flag: true
},
actionHandlers: {
SET_MSG: (state, msg: string) => ({ msg }),
SET_NUM: (state, num: number) => ({ num }),
SET_FLAG: (state, flag: boolean) => ({ flag })
} as const, // <---------------------------------------- here
helpers: {
setFlagAsync: async (dispatch, flag: boolean) => {
await Promise.resolve();
dispatch("SET_FLAG", flag);
}
}
});
That way you can indeed define your function arguments within the function call.