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
Several comments above have mentioned concept/function `actionCreator´ - take a look at redux-actions package (and corresponding TypeScript definitions), that solves first part of the problem: creating action creator functions that have TypeScript type information specifying action payload type.
Second part of the problem is combining reducer functions into single reducer without boilerplate code and in a type-safe manner (as the question was asked about TypeScript).
Combine redux-actions and redux-actions-ts-reducer packages:
1) Create actionCreator functions that can be used for creating action with desired type and payload when dispatching the action:
import { createAction } from 'redux-actions';
const negate = createAction('NEGATE'); // action without payload
const add = createAction<number>('ADD'); // action with payload type `number`
2) Create reducer with initial state and reducer functions for all related actions:
import { ReducerFactory } from 'redux-actions-ts-reducer';
// type of the state - not strictly needed, you could inline it as object for initial state
class SampleState {
count = 0;
}
// creating reducer that combines several reducer functions
const reducer = new ReducerFactory(new SampleState())
// `state` argument and return type is inferred based on `new ReducerFactory(initialState)`.
// Type of `action.payload` is inferred based on first argument (action creator)
.addReducer(add, (state, action) => {
return {
...state,
count: state.count + action.payload,
};
})
// no point to add `action` argument to reducer in this case, as `action.payload` type would be `void` (and effectively useless)
.addReducer(negate, (state) => {
return {
...state,
count: state.count * -1,
};
})
// chain as many reducer functions as you like with arbitrary payload types
...
// Finally call this method, to create a reducer:
.toReducer();
As You can see from the comments You don't need to write any TypeScript type annotations, but all types are inferred
(so this even works with noImplicitAny TypeScript compiler option)
If You use actions from some framework that doesn't expose redux-action action creators (and You don't want to create them Yourself either)
or have legacy code that uses strings constants for action types you could add reducers for them as well:
const SOME_LIB_NO_ARGS_ACTION_TYPE = '@@some-lib/NO_ARGS_ACTION_TYPE';
const SOME_LIB_STRING_ACTION_TYPE = '@@some-lib/STRING_ACTION_TYPE';
const reducer = new ReducerFactory(new SampleState())
...
// when adding reducer for action using string actionType
// You should tell what is the action payload type using generic argument (if You plan to use `action.payload`)
.addReducer<string>(SOME_LIB_STRING_ACTION_TYPE, (state, action) => {
return {
...state,
message: action.payload,
};
})
// action.payload type is `void` by default when adding reducer function using `addReducer(actionType: string, reducerFunction)`
.addReducer(SOME_LIB_NO_ARGS_ACTION_TYPE, (state) => {
return new SampleState();
})
...
.toReducer();
so it is easy to get started without refactoring Your codebase.
You can dispatch actions even without redux like this:
const newState = reducer(previousState, add(5));
but dispatching action with redux is simpler - use the dispatch(...) function as usual:
dispatch(add(5));
dispatch(negate());
dispatch({ // dispatching action without actionCreator
type: SOME_LIB_STRING_ACTION_TYPE,
payload: newMessage,
});
Confession: I'm the author of redux-actions-ts-reducer that I open-sourced today.
If you need to fix your implementation exactly as you posted, this is the way how to fix it and get it working using type assertions , respectively as I show in the following:
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: ', (<IActionA>action).a) // property 'a' exists because you're using type assertion <IActionA>
case 'b':
return console.info('action b: ', (<IActionB>action).b) // property 'b' exists because you're using type assertion <IActionB>
}
}
You can learn more on section "Type Guards and Differentiating Types" of the official documentation: https://www.typescriptlang.org/docs/handbook/advanced-types.html
I suggest using AnyAction because according to Redux FAQ, every reducer is ran on every action. This is why we end up just returning the input state if the action is not one of the types. Otherwise we would never have a default case in our switches in our reducers.
See: https://redux.js.org/faq/performance#won-t-calling-all-my-reducers-for-each-action-be-slow
So therefore it is fine to just do:
import { AnyAction } from 'redux';
function myReducer(state, action: AnyAction) {
// ...
}
With Typescript v2, you can do this pretty easily using union types with type guards and Redux's own Action and Reducer types w/o needing to use additional 3rd party libs, and w/o enforcing a common shape to all actions (e.g. via payload).
This way, your actions are correctly typed in your reducer catch clauses, as is the returned state.
import {
Action,
Reducer,
} from 'redux';
interface IState {
tinker: string
toy: string
}
type IAction = ISetTinker
| ISetToy;
const SET_TINKER = 'SET_TINKER';
const SET_TOY = 'SET_TOY';
interface ISetTinker extends Action<typeof SET_TINKER> {
tinkerValue: string
}
const setTinker = (tinkerValue: string): ISetTinker => ({
type: SET_TINKER, tinkerValue,
});
interface ISetToy extends Action<typeof SET_TOY> {
toyValue: string
}
const setToy = (toyValue: string): ISetToy => ({
type: SET_TOY, toyValue,
});
const reducer: Reducer<IState, IAction> = (
state = { tinker: 'abc', toy: 'xyz' },
action
) => {
// action is IAction
if (action.type === SET_TINKER) {
// action is ISetTinker
// return { ...state, tinker: action.wrong } // doesn't typecheck
// return { ...state, tinker: false } // doesn't typecheck
return {
...state,
tinker: action.tinkerValue,
};
} else if (action.type === SET_TOY) {
return {
...state,
toy: action.toyValue
};
}
return state;
}
Things is basically what @Sven Efftinge suggests, while additionally checking the reducer's return type.
Lately I have been using this approach:
export abstract class PlainAction {
public abstract readonly type: any;
constructor() {
return Object.assign({}, this);
}
}
export abstract class ActionWithPayload<P extends object = any> extends PlainAction {
constructor(public readonly payload: P) {
super();
}
}
export class BeginBusyAction extends PlainAction {
public readonly type = "BeginBusy";
}
export interface SendChannelMessageActionPayload {
message: string;
}
export class SendChannelMessageAction
extends ActionWithPayload<SendChannelMessageActionPayload>
{
public readonly type = "SendChannelMessage";
constructor(
message: string,
) {
super({
message,
});
}
}
This here:
constructor() {
return Object.assign({}, this);
}
ensures that the Actions are all plain objects. Now you can make actions like this: const action = new BeginBusyAction(). (yay \o/)
I am the author of ts-redux-actions-reducer-factory and would present you this as an another solution on top of the others. This package infers the action by action creator or by manually defined action type and - that's new - the state. So each reducer takes aware of the return type of previous reducers and represents therefore a possible extended state that must be initialized at the end, unless done at beginning. It is kind of special in its use, but can simplify typings.
But here a complete possible solution on base of your problem:
import { createAction } from "redux-actions";
import { StateType } from "typesafe-actions";
import { ReducerFactory } from "../../src";
// Type constants
const aType = "a";
const bType = "b";
// Container a
interface IActionA {
a: string;
}
// Container b
interface IActionB {
b: string;
}
// You define the action creators:
// - you want to be able to reduce "a"
const createAAction = createAction<IActionA, string>(aType, (a) => ({ a }));
// - you also want to be able to reduce "b"
const createBAction = createAction<IActionB, string>(aType, (b) => ({ b }));
/*
* Now comes a neat reducer factory into the game and we
* keep a reference to the factory for example purposes
*/
const factory = ReducerFactory
.create()
/*
* We need to take care about other following reducers, so we normally want to include the state
* by adding "...state", otherwise only property "a" would survive after reducing "a".
*/
.addReducer(createAAction, (state, action) => ({
...state,
...action.payload!,
}))
/*
* By implementation you are forced to initialize "a", because we
* now know about the property "a" by previous defined reducer.
*/
.addReducer(createBAction, (state, action) => ({
...state,
...action.payload!,
}))
/**
* Now we have to call `acceptUnknownState` and are forced to initialize the reducer state.
*/
.acceptUnknownState({
a: "I am A by default!",
b: "I am B by default!",
});
// At the very end, we want the reducer.
const reducer = factory.toReducer();
const initialState = factory.initialKnownState;
// { a: "I am A by default!", b: "I am B by default!" }
const resultFromA = reducer(initialState, createAAction("I am A!"));
// { a: "I am A!", b: "I am B by default!" }
const resultFromB = reducer(resultFromA, createBAction("I am B!"));
// { a: "I am A!", b: "I am B!" }
// And when you need the new derived type, you can get it with a module like @typesafe-actions
type DerivedType = StateType<typeof reducer>;
// Everything is type-safe. :)
const derivedState: DerivedType = initialState;