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 base interface with a property type. The extended action interfaces can have more properties that are all different between the action interfaces. Here is an example below:
interface IAction {
type: string
}
interface IActionA extends IAction {
a: string
}
interface IActionB extends IAction {
b: string
}
const reducer = (action: IAction) {
switch (action.type) {
case 'a':
return console.info('action a: ', action.a) // property 'a' does not exists on type IAction
case 'b':
return console.info('action b: ', action.b) // property 'b' does not exists on type IAction
}
}
The problem is that action
needs to be cast as a type that has access to both IActionA
and IActionB
so the reducer can use both action.a
and action.a
without throwing an error.
I have several ideas how to work around this issue:
- Cast
action
toany
. - Use optional interface members.
example:
interface IAction {
type: string
a?: string
b?: string
}
- Use different reducers for every action type.
What is the best way to organize Action/Reducers in typescript? Thank you in advance!
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:
you can define your action something like:
and so, you can define your reducer like follows:
// src/reducers/index.tsx
Complete official docs: https://github.com/Microsoft/TypeScript-React-Starter#adding-a-reducer
For a relatively simple reducer you could probably just use type guards:
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
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 clunkierisType(action, createType)
.type Action<TPayload>
,interface IActionCreator<P>
,function actionCreator<P>()
,function isType<P>()
.class MyAction extends Action<{myProp}> {}
.type
property, by just calculatingtype
to be the class/constructor name. This adheres to the DRY principle, unlike the other solution which has both ahelloWorldAction
function and aHELLO_WORLD
"magic string".Anyway, to implement this alternate setup:
First, copy this generic Action class:
Then create your derived Action classes:
Then, to use in a reducer function:
When you want to create and dispatch an action, just do:
As with @Jussi_K's solution, each of these steps is type-safe.
EDIT
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:
And use it like so:
The other option is to add the
Action.Is()
method onto the globalObject.prototype
usingObject.defineProperty
. This is what I'm currently doing -- though most people don't like this since it pollutes the prototype.EDIT 2
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)Here's a clever solution from Github user aikoven from https://github.com/reactjs/redux/issues/992#issuecomment-191152574:
Use
actionCreator<P>
to define your actions and action creators:Use the user defined type guard
isType<P>
in the reducer:And to dispatch an action:
I recommend reading through the whole comment thread to find other options as there are several equally good solutions presented there.