What is the best way to cast the action parameter in a redux reducer with typescript? There will be multiple action interfaces that can occur that all extend a
Here's the approach I've taken for this problem:
const reducer = (action: IAction) {
const actionA: IActionA = action as IActionA;
const actionB: IActionB = action as IActionB;
switch (action.type) {
case 'a':
// Only ever use actionA in this context
return console.info('action a: ', actionA.a)
case 'b':
// Only ever use actionB in this context
return console.info('action b: ', actionB.b)
}
}
I'll be the first to admit there's a certain ugliness and hackiness to this approach, but I've actually found it to work pretty well in practice. In particular, I find that it makes the code easy to read and maintain because the action's intent is in the name and that also makes it easy to search for.
There are libraries that bundle most of the code mentioned in other answers: aikoven/typescript-fsa and dphilipson/typescript-fsa-reducers.
With these libraries all your actions and reducers code is statically typed and readable:
import actionCreatorFactory from "typescript-fsa";
const actionCreator = actionCreatorFactory();
interface State {
name: string;
balance: number;
isFrozen: boolean;
}
const INITIAL_STATE: State = {
name: "Untitled",
balance: 0,
isFrozen: false,
};
const setName = actionCreator<string>("SET_NAME");
const addBalance = actionCreator<number>("ADD_BALANCE");
const setIsFrozen = actionCreator<boolean>("SET_IS_FROZEN");
...
import { reducerWithInitialState } from "typescript-fsa-reducers";
const reducer = reducerWithInitialState(INITIAL_STATE)
.case(setName, (state, name) => ({ ...state, name }))
.case(addBalance, (state, amount) => ({
...state,
balance: state.balance + amount,
}))
.case(setIsFrozen, (state, isFrozen) => ({ ...state, isFrozen }));
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;
}
For a relatively simple reducer you could probably just use type guards:
function isA(action: IAction): action is IActionA {
return action.type === 'a';
}
function isB(action: IAction): action is IActionB {
return action.type === 'b';
}
function reducer(action: IAction) {
if (isA(action)) {
console.info('action a: ', action.a);
} else if (isB(action)) {
console.info('action b: ', action.b);
}
}
To be fair there are many ways to type actions but I find this one very straight forward and has the less possible boilerplate as well (already discussed in this topic).
This approach tries to type the key called "payload" of actions.
Check this sample
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.