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
you can define your action something like:
// src/actions/index.tsx
import * as constants from '../constants'
export interface IncrementEnthusiasm {
type: constants.INCREMENT_ENTHUSIASM;
}
export interface DecrementEnthusiasm {
type: constants.DECREMENT_ENTHUSIASM;
}
export type EnthusiasmAction = IncrementEnthusiasm | DecrementEnthusiasm;
export function incrementEnthusiasm(): IncrementEnthusiasm {
return {
type: constants.INCREMENT_ENTHUSIASM
}
}
export function decrementEnthusiasm(): DecrementEnthusiasm {
return {
type: constants.DECREMENT_ENTHUSIASM
}
}
and so, you can define your reducer like follows:
// src/reducers/index.tsx
import { EnthusiasmAction } from '../actions';
import { StoreState } from '../types/index';
import { INCREMENT_ENTHUSIASM, DECREMENT_ENTHUSIASM } from '../constants/index';
export function enthusiasm(state: StoreState, action: EnthusiasmAction): StoreState {
switch (action.type) {
case INCREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: state.enthusiasmLevel + 1 };
case DECREMENT_ENTHUSIASM:
return { ...state, enthusiasmLevel: Math.max(1, state.enthusiasmLevel - 1) };
}
return state;
}
Complete official docs: https://github.com/Microsoft/TypeScript-React-Starter#adding-a-reducer
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<TPayload>, interface IActionCreator<P>, function actionCreator<P>(), function isType<P>().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<Payload> {
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<Payload2>(actionType: new(..._)=>Action<Payload2>): 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<any>) {
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<Payload>(action, actionType: new(..._)=>Action<Props>): action is Payload {
return action.type == actionType.name;
}
And use it like so:
function reducer(state, action: Action<any>) {
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({}));
I might be late to the dance but enum's FTW!
enum ActionTypes {
A: 'ANYTHING_HERE_A',
B: 'ANYTHING_HERE_B',
}
interface IActionA {
type: ActionTypes.A;
a: string;
}
interface IActionB {
type: ActionTypes.B;
b: string;
}
type IAction = IActionA | IActionB
const reducer = (action: IAction) {
switch (action.type) {
case ActionTypes.A:
return console.info('action a: ', action.a)
case ActionTypes.B:
return console.info('action b: ', action.b)
}
}
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)
}
}
To get implicit typesafety without having to write interfaces for every action, you can use this approach (inspired by the returntypeof function from here: https://github.com/piotrwitek/react-redux-typescript#returntypeof-polyfill)
import { values } from 'underscore'
/**
* action creator (declaring the return type is optional,
* but you can make the props readonly)
*/
export const createAction = <T extends string, P extends {}>(type: T, payload: P) => {
return {
type,
payload
} as {
readonly type: T,
readonly payload: P
}
}
/**
* Action types
*/
const ACTION_A = "ACTION_A"
const ACTION_B = "ACTION_B"
/**
* actions
*/
const actions = {
actionA: (count: number) => createAction(ACTION_A, { count }),
actionB: (name: string) => createAction(ACTION_B, { name })
}
/**
* create action type which you can use with a typeguard in the reducer
* the actionlist variable is only needed for generation of TAction
*/
const actionList = values(actions).map(returnTypeOf)
type TAction = typeof actionList[number]
/**
* Reducer
*/
export const reducer = (state: any, action: TAction) => {
if ( action.type === ACTION_A ) {
console.log(action.payload.count)
}
if ( action.type === ACTION_B ) {
console.log(action.payload.name)
console.log(action.payload.count) // compile error, because count does not exist on ACTION_B
}
console.log(action.payload.name) // compile error because name does not exist on every action
}
you could do the following things
if you expect one of IActionA or IActionB only, you can limit the type at least and define your function as
const reducer = (action: (IActionA | IActionB)) => {
...
}
Now, the thing is, you still have to find out which type it is. You can totally add a type property but then, you have to set it somewhere, and interfaces are only overlays over object structures. You could create action classes and have the ctor set the type.
Otherwise you have to verify the object by something else.
In your case you could use hasOwnProperty and depending on that, cast it to the correct type:
const reducer = (action: (IActionA | IActionB)) => {
if(action.hasOwnProperty("a")){
return (<IActionA>action).a;
}
return (<IActionB>action).b;
}
This would still work when compiled to JavaScript.