How to type Redux actions and Redux reducers in Ty

2019-03-15 01:05发布

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:

  1. Cast action to any.
  2. Use optional interface members.

example:

interface IAction {
    type: string
    a?: string
    b?: string
}
  1. Use different reducers for every action type.

What is the best way to organize Action/Reducers in typescript? Thank you in advance!

18条回答
我欲成王,谁敢阻挡
2楼-- · 2019-03-15 01:14

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.

查看更多
叛逆
3楼-- · 2019-03-15 01:17

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;
}
查看更多
相关推荐>>
4楼-- · 2019-03-15 01:20

Here is how can you do it with redux-fluent:

enter image description here enter image description here

查看更多
兄弟一词,经得起流年.
5楼-- · 2019-03-15 01:21

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)          
    }
}
查看更多
地球回转人心会变
6楼-- · 2019-03-15 01:21

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;
查看更多
乱世女痞
7楼-- · 2019-03-15 01:22

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.

查看更多
登录 后发表回答