如何在TypeScript中键入Redux操作和Redux Reducer?

Rom*_*nko 30 typescript redux

action使用typescript在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         
    }
}
Run Code Online (Sandbox Code Playgroud)

问题是,action需要定投作为同时访问一个类型IActionA,并IActionB因此减速机可以同时使用action.a,并action.a不会引发错误.

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

  1. 铸造actionany.
  2. 使用可选的接口成员.

例:

interface IAction {
    type: string
    a?: string
    b?: string
}
Run Code Online (Sandbox Code Playgroud)
  1. 为每种操作类型使用不同的reducer.

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

Sve*_*nge 32

使用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)          
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 现在[官方文档](https://redux.js.org/usage/usage-with-typescript#avoid-action-type-unions)不鼓励这样做,而是建议使用类型谓词函数或 redux-工具包的`createSlice` (7认同)

Elm*_*mer 12

我有一个Action界面

export interface Action<T, P> {
    readonly type: T;
    readonly payload?: P;
}
Run Code Online (Sandbox Code Playgroud)

我有一个createAction功能:

export function createAction<T extends string, P>(type: T, payload: P): Action<T, P> {
    return { type, payload };
}
Run Code Online (Sandbox Code Playgroud)

我有一个动作类型常量:

const IncreaseBusyCountActionType = "IncreaseBusyCount";
Run Code Online (Sandbox Code Playgroud)

我有一个动作界面(查看酷的用法typeof):

type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;
Run Code Online (Sandbox Code Playgroud)

我有一个动作创建者功能:

function createIncreaseBusyCountAction(): IncreaseBusyCountAction {
    return createAction(IncreaseBusyCountActionType, null);
}
Run Code Online (Sandbox Code Playgroud)

现在我的reducer看起来像这样:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我每个动作都有一个reducer函数:

function reduceIncreaseBusyCountAction(state: number, action: IncreaseBusyCountAction): number {
    return state + 1;
}
Run Code Online (Sandbox Code Playgroud)


小智 9

以下是来自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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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)) {
        ...
Run Code Online (Sandbox Code Playgroud)

并发出一个动作:

dispatch(helloWorldAction({foo: 'world'})
dispatch(otherAction({a: 42, b: 'moon'}))
Run Code Online (Sandbox Code Playgroud)

我建议阅读整个评论主题以找到其他选项,因为那里有几个同样好的解决方案.


ale*_*ero 7

我可能会迟到,但是enumFTW!

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一种干净的方法 (2认同)

ats*_*u85 6

问题的两部分

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

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

解决方案

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

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

import { createAction } from 'redux-actions';

const negate = createAction('NEGATE'); // action without payload
const add = createAction<number>('ADD'); // action with payload type `number`
Run Code Online (Sandbox Code Playgroud)

2)为所有相关操作创建具有初始状态和reducer函数的reducer:

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();
Run Code Online (Sandbox Code Playgroud)

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

如果您使用来自某个框架的动作,而这些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();
Run Code Online (Sandbox Code Playgroud)

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

调度动作

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

const newState = reducer(previousState, add(5));
Run Code Online (Sandbox Code Playgroud)

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

dispatch(add(5));
dispatch(negate());
dispatch({ // dispatching action without actionCreator
    type: SOME_LIB_STRING_ACTION_TYPE,
    payload: newMessage,
});
Run Code Online (Sandbox Code Playgroud)

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


CMC*_*kai 6

我建议使用AnyAction,因为根据 Redux FAQ,每个减速器都会在每个操作上运行。这就是为什么如果操作不是其中一种类型,我们最终只返回输入状态。否则,我们的减速器中的开关永远不会出现默认情况。

请参阅:https://redux.js.org/faq/performance#won-t-calling-all-my-reducers-for-each-action-be-slow

因此,这样做就可以了:

import { AnyAction } from 'redux';

function myReducer(state, action: AnyAction) {
  // ...
}
Run Code Online (Sandbox Code Playgroud)


Vad*_*gon 5

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

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);
  }
}
Run Code Online (Sandbox Code Playgroud)


cod*_*elt 5

我是这样做的:

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;
}
Run Code Online (Sandbox Code Playgroud)

UserAction.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,
        };
    }

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

IUserReducerState.ts

import UserModel from './models/UserModel';

export default interface IUserReducerState {
    readonly currentUser: UserModel;
    readonly isLoadingUser: boolean;
}
Run Code Online (Sandbox Code Playgroud)

UserSaga.ts

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

}
Run Code Online (Sandbox Code Playgroud)

UserService.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];
    }

}
Run Code Online (Sandbox Code Playgroud)

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


Mic*_*haC 2

你可以做以下事情

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

const reducer = (action: (IActionA | IActionB)) => {
   ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

const reducer = (action: (IActionA | IActionB)) => {
    if(action.hasOwnProperty("a")){
        return (<IActionA>action).a;
    }

    return (<IActionB>action).b;
}
Run Code Online (Sandbox Code Playgroud)

当编译为 JavaScript 时,这仍然有效。