【问题标题】:How can I create typesafe higher-order Redux action creators?如何创建类型安全的高阶 Redux 动作创建者?
【发布时间】:2018-11-22 12:43:04
【问题描述】:

我遵循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?处理这些增强操作的类型安全化简器会是什么样子?

【问题讨论】:

  • 请查看redux-fluent 以获得类型安全的开发人员体验。

标签: typescript redux


【解决方案1】:

该错误有点误导,但原因是您试图在需要泛型类型的地方分配对象文字,A extends FluxStandardAction,但这可能意味着除了typemeta,@ 987654324@ 可能有其他成员,因此编译器无法真正检查对象字面量是否符合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 成为可选的,因为如果增加了该属性,则该属性将存在,​​我会更改函数的约束(尽管您应该看到它如何与您的代码库的其余部分进行交互):

function addMetaAlpha<A extends { type: string }>(action: () => A, alpha: string) {
    return function (): A & MetaAlpha {
        let act = action();
        return Object.assign(act, {
            meta: {
                alpha
            }
        })
    }
}

var metaAct1 = addMetaAlpha(action1, "DD"); // () => Action1 & MetaAlpha
var metaAct2 : () => FluxStandardAction<'one', { alpha: string }> =  metaAct1; // Is is assignable to the interface if we need it to be;

至于这是否是执行 HOC 操作的标准方式,我不能说,但它似乎确实是一种有效的方式。

【讨论】:

    【解决方案2】:

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

    我还尝试过尝试更完全地利用类型系统,但我发现我需要添加 other 类型保护以从通用 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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-08-19
      • 1970-01-01
      • 2019-03-20
      相关资源
      最近更新 更多