Observable <t>中的缩小类型

时间:2017-09-19 18:52:09

标签: typescript redux rxjs redux-observable

简短说明

我希望能够写actions$.ofType<MyAction>().map(a => ...)代替actions$.filter(a => a.type === ActionType.MY_ACTION).map((a: MyAction) => ...)

背景

我按照本文中列出的模式https://spin.atomicobject.com/2017/07/24/redux-action-pattern-typescript/尝试在Angular中使用Redux时保存一些样板文件。 TL; DR是您可以在操作的类型字段中使用字符串枚举来简化reducer代码而不会牺牲类型安全性。它涉及制作如下所示的动作界面:

export enum ActionType {
    ADD = "Add",
    NEGATE = "Negate"
}

export interface AddAction extends Action {
    type: ActionType.ADD;
    amount: number;
}

export interface NegateAction extends Action {
    type: ActionType.NEGATE;
}

export type EveryAction = AddAction | NegateAction | DummyAction;

在Reducer中,您现在可以编写如下代码:

switch (action.type) {
    case TypeKeys.ADD: {
        // The type of action is narrowed to AddAction here.
    }
    case TypeKeys.NEGATE: {
        // ...and to NegateAction here.
    }
}

问题

我想在写剧本时使用类似的模式&#34; epics&#34;使用redux-observable。我想要一个函数ofType<T>(),它同时按操作对象上的类型值进行过滤,并缩小为正确的派生操作类型。注意,redux-observable已经有一个非泛型的ofType(),它将类型值作为参数,但是:1)我宁愿将Action接口类型作为泛型参数而不是类型值传递,并且2)调用内置的ofType()之后的动作类型仍然只是Action。

这就是我想要的代码:

export class Epics {
    epics: Array<Epic<EveryAction, RootState>>;

    constructor() {
        this.epics = [
            this.afterAdd,
            this.afterNegate
        ];
    }

    public afterAdd = (actions$: Observable<EveryAction>) => actions$
        .ofType<AddAction>()
        .do(a => console.log(`Added ${a.amount} to value`)); // a is AddAction here

    public afterNegate = (actions$: Observable<EveryAction>) => actions$
        .ofType<NegateAction>()
        .do(a => console.log("Negated value"));
}

但是,我无法弄清楚如何写ofType<T>()。我一直在使用不同的TypeScript功能,如索引类型和keyof,但我无法让它工作。有可能吗?

我希望函数为ofType<T>(),但如果它必须是ofType(ActionType.INCREMENT)那么那也很好,只要它自动缩小动作类型而不必指定泛型类型和类型键。

3 个答案:

答案 0 :(得分:3)

如果您愿意将Action作为课程进行调整,那么您可以利用缩小班级类型和a definition of filter来接受类型警卫。另请注意,这会忽略使用ActionTypes枚举进行模式匹配的模式。

最重要的是,filter中的箭头功能在没有明确的类型后卫的情况下不会缩小,即使它包裹了一个类型后卫,因此工厂方法使我们的生活更加轻松:

function ofType<S extends EveryAction>(b: new () => S): (a: EveryAction) => a is S) {
  return (a: EveryAction): a is S => a instanceof b;
}

Action作为具有空构造函数的类,例如:

export class AddAction implements Action { // ...

然后使用就像你想要的那样好而短:

public afterAdd = (actions$: Observable<EveryAction>) => actions$
   .filter(ofType(AddAction))
   .do(a => a.amount);

您想要的一种模式匹配方法不起作用

function ofType<S extends EveryAction>(t: ActionType): (a: EveryAction) => a is S) {
  return (a: EveryAction): a is S => a.type === t;
}

// used as:
(a as Observable<EveryAction>).filter(ofType(ActionType.ADD)) // S is still EveryAction :/

不幸的是,TypeScript还不是那么聪明。

答案 1 :(得分:1)

这样的方法怎么样?你可以使它成为扩展方法。我使用redux-typescript-actions生成actionCreators,该名称已将名称更改为typescript-fsa现在

import { combineEpics, ActionsObservable } from 'redux-observable';
import { Observable } from 'rxjs/Observable';
import { isType, Action, ActionCreator } from 'redux-typescript-actions';

export function ofType<T>(action$: ActionsObservable<Action<T>>, actionCreator: ActionCreator<T>): Observable<Action<T>> {
    return action$.filter((action: ActionCreator<T>) => isType(action, actionCreator));
}

用法:

export const todoEpic = action$ => ofType(action$, actions.loadTodoStarted)

如果您不想使用typescript-fsa,则应使用is运算符编写方法,明显将Action和ActionCreator更改为您的类型。

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

答案 2 :(得分:1)

如果您使用类作为动作创建者,则可以执行您想要的操作。

动作将作为类实例创建,但该机制不依赖于它们作为类实例保留并且不使用instanceof。序列化的动作,发送到Redux DevTools然后使用时间旅行调试重放仍然可以使用该机制。仅使用类,以便存储类型的静态副本。

无论如何,这就是它的样子:

export enum ActionType {
  ADD = "Add",
  NEGATE = "Negate"
}

export class AddAction implements Action {
  static readonly type = "ADD";
  readonly type = "ADD";
  constructor (public amount: number) {}
}

请注意,静态和实例type属性均为readonly。它们需要被推断为字符串文字类型 - 你的reducer代码表明你正在使用一个有区别的联合来缩小行动。

对于ofType运算符,需要使用类型保护。因为该类包含静态type,所以您可以像这样实现运算符:

import { Observable } from "rxjs/Observable";
import { filter } from "rxjs/operator/filter";

interface ActionCtor<T extends string, A extends Action> {
  type: string;
  new (...args: any[]): A;
}

function ofType<T extends string, A extends Action>(
  this: Observable<Action>, creator: ActionCtor<T, A>
): Observable<A> {
  return filter.call(this, (a: Action): a is A => a.type === creator.type);
}

Observable.prototype.ofType = ofType;

declare module "rxjs/Observable" {
  interface Observable<T> {
    ofType: typeof ofType;
  }
}

您可以将操作符与此类操作创建者一起使用,它将与普通的JSON操作一起使用:

const actions = Observable
  .of<Action>({ type: "ADD", amount: 42 }, { type: "OTHER", amount: "forty-two" })
  .ofType(AddAction)
  .subscribe(a => console.log(a.amount)); // 42

最近,我也厌倦了输入这么多样板文件,所以我写了一个基于这个答案机制的库:ts-action。我还写了一篇文章,解释了与Redux行为有关的两个缩小机制:How to Reduce Action Boilerplate。并且,和你一样,我希望能够使用带有redux-observable的mechansim; ts-action及其同伴ts-action-operators会这样做。