如何创建类型安全的高阶Redux动作创建器?

时间:2018-06-13 03:24:50

标签: typescript redux

我遵循Improved Redux type safety with TypeScript 2.8中显示的模式,但我想添加一个扭曲。我的一些操作可以在不同的环境中重复使用,但需要一些额外的识别信息,以便在reducer中对它们进行排序。

我以为我可以通过添加一些高阶动作创建器修饰符来解决这个问题并保持我的代码简短。这些将接收现有的动作创建者并返回一个新的动作创建者,该动作创建者会将一些信息附加到我的Flux标准动作的meta键。

// Removed `payload` and `error` for this reproduction
interface FluxStandardAction<T extends string, Meta> {
    type: T;
    meta?: Meta;
}

// Our basic actions

enum ActionType {
    One = "one",
    Two = "two",
}

interface Action1 {
    type: ActionType.One;
}

interface Action2 {
    type: ActionType.Two;
}

type Action = Action1 | Action2;

function action1(): Action1 {
    return { type: ActionType.One }
}

function action2(): Action2 {
    return { type: ActionType.Two }
}

// Higher order action modifiers that augment the meta

interface MetaAlpha {
    meta: {
        alpha: string;
    }
}

function addMetaAlpha<T extends string, M extends {}, A extends FluxStandardAction<T, M>>(action: () => A, alpha: string) {
    return function (): A & MetaAlpha {
        let { type, meta } = action();
        return { type, meta }; // Error here
    }
}

这会产生错误:

Type '{ type: T; meta: M; }' is not assignable to type 'A & MetaAlpha'.
  Object literal may only specify known properties, but 'type' does not exist in type 'A & MetaAlpha'. Did you mean to write 'type'?

虽然我很高兴能够更好地理解这个错误消息,但我的问题是关于哪些技术适合构建高阶动作创建者。

meta键是否是实现高阶动作创建者的合适方式?如果是这样,我怎么能以编译器满意的方式实现addMetaAlpha?处理这些增强动作的类型安全减速器看起来像什么?

2 个答案:

答案 0 :(得分:1)

错误有点误导,但原因是您试图分配一个对象文字,其中包含泛型类型A extends FluxStandardAction,但这可能意味着type和{ {1}},meta可能有其他成员,因此编译器无法真正检查对象文字是否符合A的形状。以下赋值在您编写的函数中有效,因为属性是已知的,因此可以检查:

A

如果要返回包含所有原始属性的对象,并使用新let result : FluxStandardAction<T, M> = { type: type, meta }; 属性,则可以使用meta,因为这将返回类型参数的交集类型。

Object.assign

此外,我不会使function addMetaAlpha<A extends FluxStandardAction<string, any>>(action: () => A, alpha: string) { return function (): A & MetaAlpha { let act = action(); return Object.assign(act, { meta: { alpha } }) } } var metaAct1 = addMetaAlpha(action1, "DD"); // () => Action1 & MetaAlpha 成为可选项,因为如果操作被扩充,属性将存在,​​我将更改函数的约束(尽管您应该看到它如何与代码库的其余部分交互) :

meta

至于这是否是做HOC行为的标准方式,我无法说明这一点,但它似乎确实是一种有效的方式。

答案 1 :(得分:0)

使用provided solution for my action creators之后,我对减速器的方向略有不同。

我没有尝试在高阶减速器中完全编码动作的变化类型,而是使用类型保护来测试动作是否“秘密地”携带其中一条元信息。基于此,我可以调用底层的reducer。

// Basic actions

// Removed payload and error for this demonstration
interface FluxStandardAction<T extends string, Meta = undefined> {
    type: T;
    meta?: Meta;
}

enum ActionType {
    One = "one",
    Two = "two",
}

const action1 = () => ({ type: ActionType.One });
const action2 = () => ({ type: ActionType.Two });

type Action =
    | ReturnType<typeof action1>
    | ReturnType<typeof action2>
    ;

// Higher order action modifiers that augment the action's meta properties

interface WithGreekLetter {
    meta: {
        greek: string;
    }
}

const withGreekLetter = <T extends string, M extends {}, A extends FluxStandardAction<T, M>>(action: () => A, greek: string) =>
    (): A & WithGreekLetter => {
        let act = action();
        let meta = Object.assign({}, act.meta, { greek });
        return Object.assign(act, { meta });
    }

const isWithGreekLetter = (a: any): a is WithGreekLetter =>
    a['meta'] && a['meta']['greek'];

// A basic reusable reducer

type State = number;
const initialState: State = 0;

function plainReducer(state: State, action: Action): State {
    switch (action.type) {
        case ActionType.One:
            return state + 1;
        case ActionType.Two:
            return state + 2;
        default:
            return state;
    }
}

// The higher-order reducer

const forGreekLetter = <S, A>(reducer: (state: S, action: A) => S, greek: string) =>
    (state: S, action: A) =>
        isWithGreekLetter(action) && action.meta.greek === greek ? reducer(state, action) : state;

// Build the concrete action creator and reducer instances

const ALPHA = 'alpha';
const BETA = 'beta';

let oneA = withGreekLetter(action1, ALPHA);
let oneB = withGreekLetter(action1, BETA);
let twoA = withGreekLetter(action2, ALPHA);
let twoB = withGreekLetter(action2, BETA);

let reducerAlphaNoInitial = forGreekLetter(plainReducer, ALPHA);
let reducerA = (state = initialState, action: Action) => reducerAlphaNoInitial(state, action);

let reducerBetaNoInitial = forGreekLetter(plainReducer, BETA);
let reducerB = (state = initialState, action: Action) => reducerBetaNoInitial(state, action);

// Exercise the action creators and reducers

let actions = [oneB(), oneA(), twoB(), twoA(), twoB()];

let stateA: State | undefined = undefined;
let stateB: State | undefined = undefined;

for (const action of actions) {
    stateA = reducerA(stateA, action);
    stateB = reducerB(stateB, action);
}

console.log({ stateA, stateB });
// {stateA: 3, stateB: 5}

我还试图更充分地利用类型系统,但我发现我需要添加其他类型的防护来从通用FluxStandardAction转到我的具体行动。因为我必须有这些类型的防护,我觉得另一条路更简单。

如果您有兴趣更多地遵循该路径,那么这是一个将类型保护应用于动作的高阶减速器。

const isAction = (a: any): a is Action =>
    Object.values(ActionType).includes(a['type']);

export const onlySpecificAction = <S, A1, A2>(reducer: (s: S, a: A1) => S, isA: IsA<A1>) =>
    (state: S, action: A2) =>
        isA(action) ? reducer(state, action) : state;