如何在 TypeScript 中键入 Redux 操作和 Redux 减少程序?

什么是最好的方法来强制转换 action参数在一个简化的 减速器与类型脚本?可能会出现多个操作接口,这些操作接口都使用属性类型扩展基接口。扩展的操作接口可以具有更多的属性,这些属性在操作接口之间都是不同的。下面是一个例子:

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

问题是,action需要被强制转换为同时访问 IActionAIActionB的类型,这样减速器就可以同时使用 action.aaction.a而不会抛出错误。

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

  1. 播放 actionany
  2. 使用可选的接口成员。

例如:

interface IAction {
type: string
a?: string
b?: string
}
  1. 对每种动作类型使用不同的减速器。

什么是最好的方式来组织行动/减少打字机? 预先感谢你!

81308 次浏览

你可以做以下事情

如果只期望 IActionAIActionB中的一个,那么至少可以限制该类型,并将函数定义为

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

现在,问题是,你还是要找出它是哪种类型的。您完全可以添加一个 type属性,但是必须将其设置在某个位置,并且接口只是覆盖在对象结构上。您可以创建操作类并让 ctor 设置类型。

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

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


return (<IActionB>action).b;
}

当编译成 JavaScript 时,这仍然可以工作。

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

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

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

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

使用 actionCreator<P>来定义您的操作和操作创建器:

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

在减速器中使用用户定义的类型保护 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)) {
...

并采取行动:

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

我建议通读整个评论线程,找到其他选择,因为有几个同样好的解决方案。

使用类型脚本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)
}
}

下面是我对这个问题采取的方法:

const reducer = (action: IAction) {


const actionA: IActionA = action as IActionA;
const actionB: IActionB = action as IActionB;


switch (action.type) {
case 'a':
// Only ever use actionA in this context
return console.info('action a: ', actionA.a)


case 'b':
// Only ever use actionB in this context
return console.info('action b: ', actionB.b)
}
}

我将首先承认这种方法有一定的丑陋和粗糙,但我实际上已经发现它在实践中非常有效。特别是,我发现它使代码易于阅读和维护,因为动作的意图在名称中,这也使它易于搜索。

我有一个 Action接口

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

我有一个 createAction函数:

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

我有一个动作类型常数:

const IncreaseBusyCountActionType = "IncreaseBusyCount";

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

type IncreaseBusyCountAction = Action<typeof IncreaseBusyCountActionType, void>;

我有一个动作创作功能:

function createIncreaseBusyCountAction(): IncreaseBusyCountAction {
return createAction(IncreaseBusyCountActionType, null);
}

现在我的减速器看起来像这样:

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

每个动作都有一个减速器函数:

function reduceIncreaseBusyCountAction(state: number, action: IncreaseBusyCountAction): number {
return state + 1;
}

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

然而,我找到了一种我更喜欢的方法,有五点:

  1. 它直接在 action 对象上具有 action 属性,而不是在“ payload”对象中,后者更短。(如果您喜欢“ payload”道具,只需取消注释构造函数中的额外行即可)
  2. 它可以用简单的 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. 它确保 class-name 和 type属性之间的一致性,只需计算类/构造函数名称为 type即可。这符合 DRY 原则,不像其他解决方案,它同时具有 helloWorldAction函数和 HELLO_WORLD“魔术字符串”。

无论如何,要实现这个替代设置:

首先,复制这个通用的 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
}
}

然后创建派生的 Action 类:

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

然后,在一个还原函数中使用:

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

当您想要创建和分派一个操作时,只需要:

dispatch(new IncreaseNumberAction({amount: 10}));

与@Jussi _ K 的解决方案一样,这些步骤都是类型安全的。

剪辑

如果您希望系统与匿名操作对象兼容(例如,来自遗留代码或反序列化状态) ,那么您可以在 reducers 中使用这个静态函数:

function IsType<Payload>(action, actionType: new(..._)=>Action<Props>): action is Payload {
return action.type == actionType.name;
}

像这样使用它:

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

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

编辑2

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

要解决这个问题,你可以:

  1. 在 Redux 中删除 isPlainObject()检查。
  2. 在我上面的编辑中做一个修改,加上在 Action类的构造函数末尾添加这一行: (它删除了实例和类之间的运行时链接)
Object.setPrototypeOf(this, Object.getPrototypeOf({}));

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

有一些库将其他答案中提到的大部分代码捆绑在一起: 艾科文/打字稿 -FSADphilison/typeescript-fsa-reducers

使用这些库,所有的 action 和 reducers 代码都是静态类型化的并且是可读的:

import actionCreatorFactory from "typescript-fsa";
const actionCreator = actionCreatorFactory();


interface State {
name: string;
balance: number;
isFrozen: boolean;
}


const INITIAL_STATE: State = {
name: "Untitled",
balance: 0,
isFrozen: false,
};


const setName = actionCreator<string>("SET_NAME");
const addBalance = actionCreator<number>("ADD_BALANCE");
const setIsFrozen = actionCreator<boolean>("SET_IS_FROZEN");


...


import { reducerWithInitialState } from "typescript-fsa-reducers";


const reducer = reducerWithInitialState(INITIAL_STATE)
.case(setName, (state, name) => ({ ...state, name }))
.case(addBalance, (state, amount) => ({
...state,
balance: state.balance + amount,
}))
.case(setIsFrozen, (state, isFrozen) => ({ ...state, isFrozen }));

你可以这样定义你的行为:

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

因此,你可以定义你的减速器如下:

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

完整的官方文件: https://github.com/Microsoft/TypeScript-React-Starter#adding-a-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: ', (<IActionA>action).a) // property 'a' exists because you're using type assertion <IActionA>


case 'b':
return console.info('action b: ', (<IActionB>action).b) // property 'b' exists because you're using type assertion <IActionB>
}
}

您可以在“类型保护和区分类型”部分了解更多信息 官方文件的 https://www.typescriptlang.org/docs/handbook/advanced-types.html :

公平地说,有很多方法可以输入操作,但是我发现 这个非常直接,而且也有一些不太可能的样板(已经在本主题中讨论过了)。

这种方法尝试输入称为“ payload”的操作键。

检查一下这个样本

最近我一直在使用这种方法:

export abstract class PlainAction {
public abstract readonly type: any;
constructor() {
return Object.assign({}, this);
}
}


export abstract class ActionWithPayload<P extends object = any> extends PlainAction {
constructor(public readonly payload: P) {
super();
}
}


export class BeginBusyAction extends PlainAction {
public readonly type = "BeginBusy";
}


export interface SendChannelMessageActionPayload {
message: string;
}


export class SendChannelMessageAction
extends ActionWithPayload<SendChannelMessageActionPayload>
{
public readonly type = "SendChannelMessage";
constructor(
message: string,
) {
super({
message,
});
}
}

这里:

constructor() {
return Object.assign({}, this);
}

确保所有的 Action都是纯对象。现在你可以做这样的动作: const action = new BeginBusyAction()

问题的两个方面

上面的一些评论提到了概念/函数‘ actionCreator- 看看 复制行动包 (以及相应的 类型脚本定义) , 这解决了问题的第一部分: 创建具有指定操作有效负载类型的 TypeScript 类型信息的操作创建器函数。

问题的第二部分是在没有样板代码的情况下,以类型安全的方式将减速器函数组合成单个减速器 (因为这个问题是关于 TypeScript 的)。

解决办法

合并 复制行动简化作用简化剂简化作用简化剂套件:

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)为所有相关操作创建具有初始状态和减速器功能的减速器:

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

从注释中可以看出,您不需要编写任何 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();

因此,不需要重构代码库就可以很容易地开始。

调度行动

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

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

但使用 redux进行调度操作更为简单——像往常一样使用 dispatch(...)函数:

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

坦白说: 我是今天开源的 redux-actions-ts-reduce 的作者。

有了 Type escript v2,你可以很容易地使用 带型式保护装置的接头型式和 Redux 自己的 开拍减速器类型来做到这一点,但是需要使用额外的第三方库,并且需要对所有操作(例如通过 payload)强制使用一个通用的形状。

通过这种方式,您的操作在 reducercatch 子句中的类型是正确的,返回的状态也是正确的。

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

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

我是这么做的:

行动

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

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


}

用户减少器

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

import UserModel from './models/UserModel';


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

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


}

用户服务

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

我是 还原作用还原剂工厂的作者,我会把这个作为另一个解决方案呈现给你。 这个包通过操作创建者或手动定义的操作类型来推断操作,这是新的状态。因此,每个约简器都知道前一个约简器的返回类型,因此表示一个可能的扩展状态,除非在开始时进行初始化,否则必须在结束时进行初始化。它的用法有些特殊,但可以简化输入。

但这里有一个完整的 有可能解决方案,基于你的问题:

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;

下面是如何用 redux-fluent做到这一点:

enter image description here enter image description here

我建议使用 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) {
// ...
}

我可能会迟到,但 enum的 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)
}
}

我认为很多这样的答案离抽象只有两三步之遥。动作创建器工厂很不错,但是对于混合类型和其他类似的东西来说,除了类型脚本之外,其他的都非常糟糕。我要看得懂的东西。

// types
export interface IType<T = any> {
type: T;
}
export interface IPayload {
// where primitives are specifically number , string , boolean, and custom class Nothing.
storeTLD: string, storeKey: string, storeValue: TPrimitives
}
export interface IAction extends IType<string> {
type: string;
payload?: IPayload;
}
export interface IActions extends IType<string> {
type: string;
payloads: IPayload[]
}
// begin action registry
export type TSetUserDataAction = ReturnType<typeof setUserDataAction>;
export type TUserActions = TSetUserIdAction; // and other actions


export const setUserIdAction = (
payload: IUserIdPayload
): IAction => ({
type: SET_USER_ID,
payload,
});


const [state, appSingleSlotDispatch] = useReducer(appSingleSlotReducer,initialState);


appSingleSlotDispatch(setUserIdAction({destination: 'user', storeKey: 'id", storeValue: someFetchedPrimitive})


const appSingleSlotReducer = (state, action: IAction) => {
const appReduce = (state,{storeTLD,storeKey,storeValue}:IPayload) => {
const newState = {...state}
newState[storeTLD][storeKey] = storeValue
return newState
}
switch (action.type) {
case SET_USER_ID:
return appReduce(state, action.payload)
}
return state;
}
const appMultiSlotReducer = (state: IInitialState, action: IActions) => {
return action.payloads.reduce(appReduce, {...state})
}

你可能会让你的减速器超负荷工作,但同样的,它会更难阅读,你需要更多的 t 来支持它。至少,90% 的代码使用这些简单的原语简化程序。当业务逻辑需要更高级的嵌套对象时,您可以创建一个 appSuperReducer。

抽象工厂是否比 multiSlotAppReducer 更具可读性?你的巢穴深度是否超过两层?如果您的嵌套深度如此之大,那么使用 Graphql/Relit,通过它的可重用接口和易于嵌套的对象,您将获得更多的好处。

归根结底,泛型是另一种超载的方式,而超载是单一责任原则的敌人。如果我们能够可靠地确保这两个逻辑约简适用于同一个形状,那么我认为我们可以在它们之间建立一个契约,应用泛型,得到我们的多态蛋糕。但是还原状态首先受到需求变化的冲击,而需求变化是多态性的敌人。

这是我的看法。

以前的答案的问题在于你需要事先知道你所有的行为。这既是笨拙的,也是不必要的耦合。

最简单、最健壮的方法就是简单地将 type输入为 string,将 payload输入为 unknown,然后使用 类型谓词在需要它的地方断言类型:

function actionIsA(action: Action): action is IActionA {
return (action as IActionA).a !== undefined;
}


function actionIsB(action: Action): action is IActionB {
return (action as IActionB).b !== undefined;
}


const reducer = (state, action: Action) {
switch (action.type) {
case 'a':
return actionIsA(action) && console.info('action a: ', action.a); // no errors


case 'b':
return actionIsB(action) && console.info('action b: ', action.b); // also no errors
}
}


// or better (in my opinion)
const reducer = (state action: Action) {
if (actionIsA(action)) {
console.info('action a: ', action.a);
}


if (actionIsB(action)) {
console.info('action b: ', action.b);
}
}

对我来说,更重要的事情是键入 Actions 和 Dispatching 在里面我的组件/服务。 因此,我能想到的最简单的事情就是这样做(灵感来自 Redux 打字指南) :

制作一个专门用来输入动作的定制钩子:

./hooks. ts

import { useDispatch} from 'react-redux';


export const useAppDispatch: () => (action: IActionModel) => void = useDispatch

然后在我的组件中使用它:

./Component. tsx

import {useAppDispatch} from "./hooks";


export const Component () => {
...
const dispatch = useAppDispatch();
myHandler () {
const myAction : IAction = {type: 'foo', payload :'bar'};
dispatch(myAction)
// This is where your IDE/ Linter ect. will
// do the type checks and will not allow you to insert
// a badly typed Object.
}


...
}

这样做有一定的优势,那就是你只需要按照你喜欢的方式定义 IAction (包括 unioin 等所有不同的类型脚本特性) ,然后在你的组件中使用它,并进行类型检查。