How to type Redux actions and Redux reducers in TypeScript?

后端 未结 20 2019
旧时难觅i
旧时难觅i 2020-12-13 03:39

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

相关标签:
20条回答
  • 2020-12-13 04:15

    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

    0 讨论(0)
  • 2020-12-13 04:16

    The solution @Jussi_K referenced is nice because it's generic.

    However, I found a way that I like better, on five points:

    1. It has the action properties directly on the action object, rather than in a "payload" object -- which is shorter. (though if you prefer the "payload" prop, just uncomment the extra line in the constructor)
    2. It can be type-checked in reducers with a simple action.Is(Type), instead of the clunkier isType(action, createType).
    3. The logic's contained within a single class, instead of spread out amonst type Action<TPayload>, interface IActionCreator<P>, function actionCreator<P>(), function isType<P>().
    4. It uses simple, real classes instead of "action creators" and interfaces, which in my opinion is more readable and extensible. To create a new Action type, just do class MyAction extends Action<{myProp}> {}.
    5. It ensures consistency between the class-name and 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.

    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:

    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.

    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:

    1. Remove the isPlainObject() checks in Redux.
    2. Do one of the modifications in my edit above, plus add this line to the end of the Action class's constructor: (it removes the runtime link between instance and class)
    Object.setPrototypeOf(this, Object.getPrototypeOf({}));
    
    0 讨论(0)
  • 2020-12-13 04:17

    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)
        }
    }
    
    0 讨论(0)
  • 2020-12-13 04:18

    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)          
        }
    }
    
    0 讨论(0)
  • 2020-12-13 04:19

    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
    }
    
    0 讨论(0)
  • 2020-12-13 04:21

    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.

    0 讨论(0)
提交回复
热议问题