如何在 TypeScript 中输入 Redux 动作和 Redux 减速器?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/35482241/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 06:55:09  来源:igfitidea点击:

How to type Redux actions and Redux reducers in TypeScript?

typescriptredux

提问by Roman Klimenko

What is the best way to cast the actionparameter in a redux reducerwith 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:

action在带有打字稿的 redux reducer 中转换参数的最佳方法是什么?可能会出现多个操作接口,它们都扩展了具有属性类型的基本接口。扩展的动作接口可以有更多的属性,这些属性在动作接口之间都是不同的。下面是一个例子:

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 actionneeds to be cast as a type that has access to both IActionAand IActionBso the reducer can use both action.aand action.awithout throwing an error.

问题是,action需要被转换为具有这两个访问的类型IActionAIActionB这样的减速可以同时使用action.a,并action.a不会引发错误。

I have several ideas how to work around this issue:

我有几个想法如何解决这个问题:

  1. Cast actionto any.
  2. Use optional interface members.
  1. 投射actionany.
  2. 使用可选的接口成员。

example:

例子:

interface IAction {
    type: string
    a?: string
    b?: string
}
  1. Use different reducers for every action type.
  1. 为每种动作类型使用不同的减速器。

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

在打字稿中组织 Action/Reducers 的最佳方法是什么?先感谢您!

回答by Sven Efftinge

With Typescript 2's Tagged Union Typesyou can do the following

使用 Typescript 2 的标记联合类型,您可以执行以下操作

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)          
    }
}

回答by Elmer

I have an Actioninterface

我有一个Action界面

export interface Action<T, P> {
    readonly type: T;
    readonly payload?: P;
}

I have a createActionfunction:

我有一个createAction功能:

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):

我有一个操作界面(查看 的酷用法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;
}

回答by Jussi K

Here's a clever solution from Github user aikovenfrom https://github.com/reactjs/redux/issues/992#issuecomment-191152574:

这是来自https://github.com/reactjs/redux/issues/992#issuecomment-191152574 的Github 用户aikoven的一个聪明的解决方案:

type Action<TPayload> = {
    type: string;
    payload: TPayload;
}

interface IActionCreator<P> {
  type: string;
  (payload: P): Action<P>;
}

function actionCreator<P>(type: string): IActionCreator<P> {
  return Object.assign(
    (payload: P) => ({type, payload}),
    {type}
  );
}

function isType<P>(action: Action<any>,
                          actionCreator: IActionCreator<P>): action is Action<P> {
  return action.type === actionCreator.type;
}

Use actionCreator<P>to define your actions and action creators:

使用actionCreator<P>来定义你的动作和动作的创造者:

export const helloWorldAction = actionCreator<{foo: string}>('HELLO_WORLD');
export const otherAction = actionCreator<{a: number, b: string}>('OTHER_ACTION');

Use the user defined type guard isType<P>in the reducer:

isType<P>在减速器中使用用户定义的类型保护:

function helloReducer(state: string[] = ['hello'], action: Action<any>): string[] {
    if (isType(action, helloWorldAction)) { // type guard
       return [...state, action.payload.foo], // action.payload is now {foo: string}
    } 
    else if(isType(action, otherAction)) {
        ...

And to dispatch an action:

并发送一个动作:

dispatch(helloWorldAction({foo: 'world'})
dispatch(otherAction({a: 42, b: 'moon'}))

I recommend reading through the whole comment thread to find other options as there are several equally good solutions presented there.

我建议通读整个评论线程以找到其他选项,因为那里提供了几个同样好的解决方案。

回答by codeBelt

Here is how I do it:

这是我如何做到的:

IAction.ts

IAction.ts

import {Action} from 'redux';

/**
 * https://github.com/acdlite/flux-standard-action
 */
export default interface IAction<T> extends Action<string> {
    type: string;
    payload?: T;
    error?: boolean;
    meta?: any;
}

UserAction.ts

用户操作.ts

import IAction from '../IAction';
import UserModel from './models/UserModel';

export type UserActionUnion = void | UserModel;

export default class UserAction {

    public static readonly LOAD_USER: string = 'UserAction.LOAD_USER';
    public static readonly LOAD_USER_SUCCESS: string = 'UserAction.LOAD_USER_SUCCESS';

    public static loadUser(): IAction<void> {
        return {
            type: UserAction.LOAD_USER,
        };
    }

    public static loadUserSuccess(model: UserModel): IAction<UserModel> {
        return {
            payload: model,
            type: UserAction.LOAD_USER_SUCCESS,
        };
    }

}

UserReducer.ts

UserReducer.ts

import UserAction, {UserActionUnion} from './UserAction';
import IUserReducerState from './IUserReducerState';
import IAction from '../IAction';
import UserModel from './models/UserModel';

export default class UserReducer {

    private static readonly _initialState: IUserReducerState = {
        currentUser: null,
        isLoadingUser: false,
    };

    public static reducer(state: IUserReducerState = UserReducer._initialState, action: IAction<UserActionUnion>): IUserReducerState {
        switch (action.type) {
            case UserAction.LOAD_USER:
                return {
                    ...state,
                    isLoadingUser: true,
                };
            case UserAction.LOAD_USER_SUCCESS:
                return {
                    ...state,
                    isLoadingUser: false,
                    currentUser: action.payload as UserModel,
                };
            default:
                return state;
        }
    }

}

IUserReducerState.ts

IUserReducerState.ts

import UserModel from './models/UserModel';

export default interface IUserReducerState {
    readonly currentUser: UserModel;
    readonly isLoadingUser: boolean;
}

UserSaga.ts

UserSagats

import IAction from '../IAction';
import UserService from './UserService';
import UserAction from './UserAction';
import {put} from 'redux-saga/effects';
import UserModel from './models/UserModel';

export default class UserSaga {

    public static* loadUser(action: IAction<void> = null) {
        const userModel: UserModel = yield UserService.loadUser();

        yield put(UserAction.loadUserSuccess(userModel));
    }

}

UserService.ts

用户服务.ts

import HttpUtility from '../../utilities/HttpUtility';
import {AxiosResponse} from 'axios';
import UserModel from './models/UserModel';
import RandomUserResponseModel from './models/RandomUserResponseModel';
import environment from 'environment';

export default class UserService {

    private static _http: HttpUtility = new HttpUtility();

    public static async loadUser(): Promise<UserModel> {
        const endpoint: string = `${environment.endpointUrl.randomuser}?inc=picture,name,email,phone,id,dob`;
        const response: AxiosResponse = await UserService._http.get(endpoint);
        const randomUser = new RandomUserResponseModel(response.data);

        return randomUser.results[0];
    }

}

https://github.com/codeBelt/typescript-hapi-react-hot-loader-example

https://github.com/codeBelt/typescript-hapi-react-hot-loader-example

回答by Vadim Macagon

For a relatively simple reducer you could probably just use type guards:

对于相对简单的减速器,您可能只使用类型保护:

function isA(action: IAction): action is IActionA {
  return action.type === 'a';
}

function isB(action: IAction): action is IActionB {
  return action.type === 'b';
}

function reducer(action: IAction) {
  if (isA(action)) {
    console.info('action a: ', action.a);
  } else if (isB(action)) {
    console.info('action b: ', action.b);
  }
}

回答by atsu85

Two parts of the problem

问题的两部分

Several comments above have mentioned concept/function `actionCreator′ - take a look at redux-actionspackage (and corresponding TypeScript definitions), that solves first part of the problem: creating action creator functions that have TypeScript type information specifying action payload type.

上面的几条评论提到了概念/函数`actionCreator' - 看看redux-actions包(和相应的TypeScript 定义),它解决了问题的第一部分:创建具有指定操作负载类型的 TypeScript 类型信息的操作创建器函数。

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).

问题的第二部分是以类型安全的方式将 reducer 函数组合到单个 reducer 中,而没有样板代码和类型安全(因为问题是关于 TypeScript 的)。

The solution

解决方案

Combine redux-actionsand redux-actions-ts-reducerpackages:

结合 redux-actionsredux-actions-ts-reducer包:

1) Create actionCreator functions that can be used for creating action with desired type and payload when dispatching the action:

1) 创建 actionCreator 函数,可用于在分派动作时创建具有所需类型和有效载荷的动作:

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:

2)为所有相关操作创建具有初始状态和减速器功能的减速器:

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 noImplicitAnyTypeScript compiler option)

正如您从评论中看到的,您不需要编写任何 TypeScript 类型注释,但是所有类型都是推断出来的(因此这甚至适用于noImplicitAnyTypeScript 编译器选项

If You use actions from some framework that doesn't expose redux-actionaction 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:

如果您使用某些框架中的动作,而这些redux-action动作不会公开动作创建者(并且您也不想自己创建它们),或者拥有使用字符串常量作为动作类型的遗留代码,您也可以为它们添加减速器:

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.

因此无需重构代码库即可轻松入门。

Dispatching actions

调度动作

You can dispatch actions even without reduxlike this:

即使没有redux这样,您也可以调度操作:

const newState = reducer(previousState, add(5));

but dispatching action with reduxis simpler - use the dispatch(...)function as usual:

但是redux使用with 调度动作更简单 -dispatch(...)像往常一样使用该函数:

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.

忏悔:我是今天开源的 redux-actions-ts-reducer 的作者。

回答by James Conkling

With Typescript v2, you can do this pretty easily using union types with type guardsand Redux's own Actionand Reducertypes w/o needing to use additional 3rd party libs, and w/o enforcing a common shape to all actions (e.g. via payload).

使用 Typescript v2,您可以使用带有类型保护的联合类型和 Redux 自己的ActionReducer类型轻松完成此操作,而无需使用额外的 3rd 方库,并且无需为所有操作强制执行通用形状(例如 via payload)。

This way, your actions are correctly typed in your reducer catch clauses, as is the returned state.

这样,您的操作在您的减速器 catch 子句中正确键入,返回的状态也是如此。

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.

事情基本上是@Sven Efftinge 所建议的,同时还要检查减速器的返回类型。

回答by MichaC

you could do the following things

你可以做以下事情

if you expect one of IActionAor IActionBonly, you can limit the type at least and define your function as

如果您期望其中之一IActionAIActionB仅,您可以至少限制类型并将您的函数定义为

const reducer = (action: (IActionA | IActionB)) => {
   ...
}

Now, the thing is, you still have to find out which type it is. You can totally add a typeproperty 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.

现在,问题是,您仍然必须找出它是哪种类型。你可以完全添加一个type属性,但是你必须在某个地方设置它,并且接口只是覆盖在对象结构上。您可以创建动作类并让构造函数设置类型。

Otherwise you have to verify the object by something else. In your case you could use hasOwnPropertyand depending on that, cast it to the correct type:

否则,您必须通过其他方式验证对象。在您的情况下,您可以使用hasOwnProperty并根据它,将其转换为正确的类型:

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.

这在编译为 JavaScript 时仍然有效。

回答by Venryx

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

@Jussi_K 引用的解决方案很好,因为它是通用的。

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 typeproperty, by just calculating typeto be the class/constructor name. This adheres to the DRY principle, unlike the other solution which has both a helloWorldActionfunction and a HELLO_WORLD"magic string".
  1. 它直接在动作对象上具有动作属性,而不是在“有效载荷”对象中——后者更短。(尽管如果您更喜欢“有效载荷”道具,只需在构造函数中取消注释多余的行即可)
  2. 它可以在 reducer 中使用简单的action.Is(Type),而不是笨重的进行类型检查isType(action, createType)
  3. 逻辑包含在单个类中,而不是分散在type Action<TPayload>, interface IActionCreator<P>, function actionCreator<P>(), 中function isType<P>()
  4. 它使用简单、真实的类而不是“动作创建者”和接口,在我看来,它更具可读性和可扩展性。要创建新的 Action 类型,只需执行class MyAction extends Action<{myProp}> {}.
  5. type通过仅计算type为类/构造函数名称来确保类名和属性之间的一致性。这符合 DRY 原则,不像其他解决方案同时具有helloWorldAction函数和HELLO_WORLD“魔法字符串”。

Anyway, to implement this alternate setup:

无论如何,要实现此备用设置:

First, copy this generic Action class:

首先,复制这个通用的 Action 类:

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:

然后创建您的派生 Action 类:

class IncreaseNumberAction extends Action<{amount: number}> {}
class DecreaseNumberAction extends Action<{amount: number}> {}

Then, to use in a reducer function:

然后,在 reducer 函数中使用:

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.

与@Jussi_K 的解决方案一样,这些步骤中的每一步都是类型安全的。

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.prototypeusing Object.defineProperty. This is what I'm currently doing -- though most people don't like this since it pollutes the prototype.

另一种选择是使用 将Action.Is()方法添加到全局Object.prototypeObject.defineProperty。这就是我目前正在做的事情——尽管大多数人不喜欢这样做,因为它会污染原型。

EDIT 2

编辑 2

Despite the fact that it would work anyway, Redux complains that "Actions must be plain objects. Use custom middleware for async actions.".

尽管无论如何它都可以工作,但 Redux 抱怨“操作必须是普通对象。使用自定义中间件进行异步操作。”。

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 Actionclass's constructor: (it removes the runtime link between instance and class)
  1. 删除isPlainObject()Redux 中的检查。
  2. 在我上面的编辑中做一个修改,加上这一行到Action类的构造函数的末尾:(它删除了实例和类之间的运行时链接)
Object.setPrototypeOf(this, Object.getPrototypeOf({}));

回答by huesforalice

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)

要获得隐式类型安全而不必为每个操作编写接口,您可以使用这种方法(灵感来自这里的 returntypeof 函数: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
}