Preamble
Typically, I'll define a reducer
like below so that I have type safety with the dispatch
method:
const setWhatever = (state: State, payload: Partial<State>) => ({ ...state, ...payload });
// Maps an action type to an action handler
const actions = {
SET_WHATEVER: setWhatever,
}
type Action =
| { type: 'SET_WHATEVER', payload: Parameters<typeof setWhatever>[1] }
const reducer = (state: State, action: Action) => actions[action.type](state, action.payload);
const [state, dispatch] = useReducer(reducer, {});
The repetitive nature of the type Action
is slightly annoying. Is there any way for me to derive the type Action
based off of the const actions
?
Current v1 Solution
I actually have a mostly-working example of a CreateActions<T>
Mapped Type. Here's the CodeSandbox (v1) example.
type CreateActions<T extends typeof actions> = {
[K in keyof T]: {
type: K;
payload: T[K] extends (...args: any[]) => any ? Parameters<T[K]>[1] : never;
};
}[keyof T];
type Action = CreateActions<typeof actions>;
But there are a couple more things that don't make this perfect.
- The
reducer
needs an extraas never
casting to make some linting error messages go away:
/**
* The `as never` below "fixes" the following typing error message:
* ```
* Argument of type 'string | number | Partial<{ name: string; age: number; }> | undefined' is not assignable to parameter of type 'never'.
* Type 'undefined' is not assignable to type 'never'.ts(2345)
* ```
*
* QUESTION: is there a better way to fix the error message?
*/
const reducer = (state: State, action: Action): State =>
actions[action.type](state, action.payload as never);
- I'm forced to have a
payload
in the dispatch. Anyway I can make some actions not require a payload?
dispatch({ type: 'RESET', payload: undefined }); // linter complains with dispatch({ type: 'RESET' });
Current v2 Solution
Updated CodeSandbox (v2) example with the final solution thanks to @ryskajakub.
type Payload<T> = T extends (...args: any[]) => any
? Parameters<T>[1]
: never;
type CreateActions<T extends typeof actions> = {
[K in keyof T]: Payload<T[K]> extends undefined
? {
type: K;
// defined so reducer doesn't complain about missing action.payload
payload?: undefined;
}
: {
type: K;
payload: Payload<T[K]>;
};
}[keyof T];
/** Hover over Action to see the discriminated union type. */
type Action = CreateActions<typeof actions>;
const reducer: ActionHandler<Action> = (state, action): State =>
(actions[action.type] as ActionHandler<typeof action["payload"]>)(
state,
action.payload
);
So now I can call dispatch({ type: 'RESET' })
without any typing error.
There is a "gotcha" in that I can also call dispatch({ type: 'RESET', payload: undefined })
without any typing error, but I don't care about that.
CodePudding user response:
As for the first question, you can also make a conditional type based on the type of the payload. If payload is such that you want to ignore it, you can create a type where the payload won't be a part of object.
type Payload<T> = T extends (...args: any[]) => any ? Parameters<T>[1] : never
type CreateActions<T extends typeof actions> = {
[K in keyof T]: Payload<T[K]> extends undefined ? {
type: K
} : {
type: K;
payload: Payload<T[K]>;
};
}[keyof T];
It would be best if you would put the second question in separate question.