How to type Redux actions and Redux reducers in TypeScript?
With Typescript 2's Tagged Union Types you can do the following
interface ActionA {
type: 'a';
a: string
}
interface ActionB {
type: 'b';
b: string
}
type Action = ActionA | ActionB;
function reducer(action:Action) {
switch (action.type) {
case 'a':
return console.info('action a: ', action.a)
case 'b':
return console.info('action b: ', action.b)
}
}
I have an Action
interface
export interface Action<T, P> {
readonly type: T;
readonly payload?: P;
}
I have a createAction
function:
export function createAction<T extends string, P>(type: T, payload: P): Action<T, P> {
return { type, payload };
}
I have an action type constant:
const IncreaseBusyCountActionType = "IncreaseBusyCount";
And I have an interface for the action (check out the cool use of typeof
):
type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;
I have an action creator function:
function createIncreaseBusyCountAction(): IncreaseBusyCountAction {
return createAction(IncreaseBusyCountActionType, null);
}
Now my reducer looks something like this:
type Actions = IncreaseBusyCountAction | DecreaseBusyCountAction;
function busyCount(state: number = 0, action: Actions) {
switch (action.type) {
case IncreaseBusyCountActionType: return reduceIncreaseBusyCountAction(state, action);
case DecreaseBusyCountActionType: return reduceDecreaseBusyCountAction(state, action);
default: return state;
}
}
And I have a reducer function per action:
function reduceIncreaseBusyCountAction(state: number, action: IncreaseBusyCountAction): number {
return state + 1;
}
Here's a clever solution from Github user aikoven from https://github.com/reactjs/redux/issues/992#issuecomment-191152574:
type Action<TPayload> = {
type: string;
payload: TPayload;
}
interface IActionCreator<P> {
type: string;
(payload: P): Action<P>;
}
function actionCreator<P>(type: string): IActionCreator<P> {
return Object.assign(
(payload: P) => ({type, payload}),
{type}
);
}
function isType<P>(action: Action<any>,
actionCreator: IActionCreator<P>): action is Action<P> {
return action.type === actionCreator.type;
}
Use actionCreator<P>
to define your actions and action creators:
export const helloWorldAction = actionCreator<{foo: string}>('HELLO_WORLD');
export const otherAction = actionCreator<{a: number, b: string}>('OTHER_ACTION');
Use the user defined type guard isType<P>
in the reducer:
function helloReducer(state: string[] = ['hello'], action: Action<any>): string[] {
if (isType(action, helloWorldAction)) { // type guard
return [...state, action.payload.foo], // action.payload is now {foo: string}
}
else if(isType(action, otherAction)) {
...
And to dispatch an action:
dispatch(helloWorldAction({foo: 'world'})
dispatch(otherAction({a: 42, b: 'moon'}))
I recommend reading through the whole comment thread to find other options as there are several equally good solutions presented there.