代码之家  ›  专栏  ›  技术社区  ›  Shepmaster Lukas Kalbertodt

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

  •  2
  • Shepmaster Lukas Kalbertodt  · 技术社区  · 6 年前

    我正在遵循 Improved Redux type safety with TypeScript 2.8 但我想加上一个转折点。我的一些操作在不同的上下文中是可重用的,但是需要一些额外的识别信息来在Reducers中对它们进行分类。

    我想我可以通过添加一些高阶动作创建者修饰符来解决这个问题并保持代码简短。它们将采用现有的操作创建者,并返回一个新的操作创建者,该创建者将向 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'?
    

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

    关键是实现高阶动作创建者的适当方法?如果是,我如何实现 addMetaAlpha 在某种程度上,编译器是满意的?处理这些增强操作的类型安全还原程序是什么样子的?

    2 回复  |  直到 6 年前
        1
  •  1
  •   Titian Cernicova-Dragomir    6 年前

    A extends FluxStandardAction type meta A

    let result : FluxStandardAction<T, M> = { type: type, 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
    

    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;
    

        2
  •  0
  •   Shepmaster Lukas Kalbertodt    6 年前

    provided solution for my action creators

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