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
The solution @Jussi_K referenced is nice because it's generic.
However, I found a way that I like better, on five points:
action.Is(Type)
, instead of the clunkier isType(action, createType)
.type Action
, interface IActionCreator
, function actionCreator()
, function isType()
.class MyAction extends Action<{myProp}> {}
.type
property, by just calculating type
to be the class/constructor name. This adheres to the DRY principle, unlike the other solution which has both a helloWorldAction
function and a HELLO_WORLD
"magic string".Anyway, to implement this alternate setup:
First, copy this generic Action class:
class Action {
constructor(payload: Payload) {
this.type = this.constructor.name;
//this.payload = payload;
Object.assign(this, payload);
}
type: string;
payload: Payload; // stub; needed for Is() method's type-inference to work, for some reason
Is(actionType: new(..._)=>Action): this is Payload2 {
return this.type == actionType.name;
//return this instanceof actionType; // alternative
}
}
Then create your derived Action classes:
class IncreaseNumberAction extends Action<{amount: number}> {}
class DecreaseNumberAction extends Action<{amount: number}> {}
Then, to use in a reducer function:
function reducer(state, action: Action) {
if (action.Is(IncreaseNumberAction))
return {...state, number: state.number + action.amount};
if (action.Is(DecreaseNumberAction))
return {...state, number: state.number - action.amount};
return state;
}
When you want to create and dispatch an action, just do:
dispatch(new IncreaseNumberAction({amount: 10}));
As with @Jussi_K's solution, each of these steps is type-safe.
If you want the system to be compatible with anonymous action objects (eg, from legacy code, or deserialized state), you can instead use this static function in your reducers:
function IsType(action, actionType: new(..._)=>Action): action is Payload {
return action.type == actionType.name;
}
And use it like so:
function reducer(state, action: Action) {
if (IsType(action, IncreaseNumberAction))
return {...state, number: state.number + action.amount};
if (IsType(action, DecreaseNumberAction))
return {...state, number: state.number - action.amount};
return state;
}
The other option is to add the Action.Is()
method onto the global Object.prototype
using Object.defineProperty
. This is what I'm currently doing -- though most people don't like this since it pollutes the prototype.
Despite the fact that it would work anyway, Redux complains that "Actions must be plain objects. Use custom middleware for async actions.".
To fix this, you can either:
isPlainObject()
checks in Redux.Action
class's constructor: (it removes the runtime link between instance and class)Object.setPrototypeOf(this, Object.getPrototypeOf({}));