I'm following an action/reducer pattern for React put forth by Kent Dodds and I'm trying to add some type safety to it.
export type Action =
{ type: "DO_SOMETHING", data: { num: Number } } |
{ type: "DO_SOMETHING_ELSE", data: { nums: Number[] } };
type Actions = {
[key in Action["type"]]: (state: State, data: Action["data"]) => State;
};
const actions: Actions = {
DO_SOMETHING: (state, data) => {
return { nums: [data.num] }; // Type error
},
DO_SOMETHING_ELSE: (state, data) => {
return { nums: data.nums }; // Type error
}
};
This code is nice because it ensures the actions
object contains all the action types listed in the Action
union type as well as providing type safety when trying to dispatch an action. The problem comes in when trying to access members of data
.
Property 'num' does not exist on type '{ num: Number; } | { nums: Number[]; }'.
Property 'num' does not exist on type '{ nums: Number[]; }'.
But, if I do this:
export type Action =
{ type: "DO_SOMETHING", data: { num: Number } } |
{ type: "DO_SOMETHING_ELSE", data: { nums: Number[] } };
type Actions = {
[key in Action["type"]]: (state: State, action: Action) => State;
};
const actions: Actions = {
DO_SOMETHING: (state, action) => {
if (action.type !== "DO_SOMETHING") return state;
return { nums: [action.data.num] }; // No more type error
},
DO_SOMETHING_ELSE: (state, action) => {
if (action.type !== "DO_SOMETHING_ELSE") return state;
return { nums: action.data.nums }; // No more type error
}
};
Now TypeScript knows action.data
is the union type that matches the explicit action.type
. Is there a cleaner way to do this without having to inline all the actions into a big switch statement?
PS - Here is the full playground snippet I've been using to test all this.