Recently, I've adopted the action/reducer pattern for React based on Kent Dodds' approach and now I'm exploring ways to enhance type safety within 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] }; // Encountering a type error here
},
DO_SOMETHING_ELSE: (state, data) => {
return { nums: data.nums }; // Also resulting in a type error
}
};
The beauty of this code lies in its ability to guarantee that the actions
object includes all the defined action types from the union type Action
, while offering type safety when dispatching an action. The issue arises 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[]; }'.
However, by structuring the code as follows:
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] }; // Resolved the previous type error
},
DO_SOMETHING_ELSE: (state, action) => {
if (action.type !== "DO_SOMETHING_ELSE") return state;
return { nums: action.data.nums }; // No more type error encountered
}
};
By adopting this structure, TypeScript is now able to recognize that action.data
corresponds exactly with the specified action.type
. Is there a more streamlined approach to achieve this without resorting to extensive inline actions within a switch statement?
PS - To delve deeper into these concepts, you may explore the comprehensive playground snippet I've been utilizing for testing purposes.