How to type Redux actions and Redux reducers in TypeScript?

后端 未结 20 2000
旧时难觅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: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, interface IActionCreator

      , function actionCreator

      (), function isType

      ().

    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 {
        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(actionType: new(..._)=>Action): 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) {
        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(action, actionType: new(..._)=>Action): action is Payload {
        return action.type == actionType.name;
    }
    

    And use it like so:

    function reducer(state, action: Action) {
        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({}));
    

提交回复
热议问题