I'm using react
useReducer
and have a problem with generic types.
I want to have a generic dispatch
, but typescript makes my dispatch
not generic.
Here is my playground
function reducer<T extends keyof State>(
state: State,
action: Action<T>
): State {
return { ...state, [action.type]: action.payload };
}
const [state, dispatch] = useReducer(reducer, {
state1: "",
state2: 1,
});
The problem is the type of dispatch
, its Dispatch<Action<keyof State>>
instead of Dispatch<Action<T>>
Why the return of useReducer
is not generic? it's use infer
to extract type from reducer
parameters but it's not extracting it as generic.
And how to makes dispatch
generic?
CodePudding user response:
TypeScript can't easily synthesize the sort of higher-order generic types you would need for useReducer
's output to continue to be generic. The compiler gives up and replaces the generic type parameter T
with its constraint keyof State
, and thus you're dealing with Action<keyof State>
, a single object type whose type
and payload
properties are no longer separated properly.
Luckily, given this example, you don't have to try to get dispatch
to be generic, because you can avoid making reducer
and Action
generic. Instead, you can make Action
a union type of your original Action<T>
type for every T
in keyof State
. Like this:
type Action = { [T in keyof State]-?: {
type: T;
payload: State[T];
} }[keyof State];
// evaluates to the union:
/* type Action = {
type: "state1";
payload: string;
} | {
type: "state2";
payload: number;
} */
That technique, where you make a mapped type and then immediately index into it to get a union, is known as making a "distributive object type", as coined in microsoft/TypeScript#47109.
And now reducer()
doesn't need to be generic because its action
parameter is not generic:
function reducer(
state: State,
action: Action
): State {
return { ...state, [action.type]: action.payload };
}
const [state, dispatch] = useReducer(reducer, {
state1: "",
state2: 1,
});
And now dispatch()
is a Dispatch<Action>
, meaning it only accepts Action
arguments, which can no longer accept mixed type
and payload
properties:
dispatch({ type: "state2", payload: 123 }); // okay
dispatch({ type: "state2", payload: "" }) //error