TypeScript + React最佳实践-第二节:Redux类型化(2)——如何实现类型安全的model(1)

525 阅读2分钟

前言

TypeScript + React 类型安全三件套:Component、Redux、和Service类型化。

redux 类型化(1) 里提到了 model——一种组织 state、reducers 和 effects 代码的数据结构,本节将介绍如何实现一个类型化 model 工厂。

Redux类型化——如何实现类型安全的model

Basic Interface

定义类型 Action:

interface Action<P> {
    payload: P;
}

参照 dvajs model 的接口设计,一个 model 最核心的字段至少应该包括 state, reducers 和 effects:

interface Model {
    state: any;
    reducers: {
        [doSomething: string]: (state: any, action: Action<any>) => any;
    };
    effects: {
        [doSomethingAsync: string]: (action: Action<any>, utils: SagaUtils) => Iterator<any, any>;
    };
}

其中 Model.statereuders.doSomething.state 应该是贯通的,后者的类型,应该由前者推断可得,这时就需要泛型:

interface Model<S extends any> {
    state: S;
    reducers: {
        [doSomething: string]: (state: S, action: Action<any>) => S;
    };
    effects: {
        [doSomethingAsync: string]: (action: Action<any>, utils: SagasUtils) => Iterator<{}, any, any>;
    };
}

则函数 app.model 类型则可以是这样:

interface CreateModel {
  <S>(model: Model<S>): {};
}

App Model

这样我们就得到一个简陋的类型安全 model 工厂:

const UserModel = app.model<{ id?: number; name?: string }>({
    state: {},
    reducers: {
        // TODO: 泛型的痛点
        doUser: (state, action: Action<number>) => ({ ...state, id: action.payload })
    },
    effects: {
        *doUserAsync(action: Action<number>, utils): Iteractor<{}, any, any> {
            // TODO: 非类型化
            const user = yield utils.call(xxx, action.payload);
            // TODO: 非类型化
            yield utils.put('user/doUser');
        }
    }
})

因为我们在 model 的类型定义里,显式的标注了每个 effect 第二个参数的类型,以及通过泛型贯通了每个 reducer 第一个参数和 state 的类型,因而实际在编写 model 代码的时候,仅需需要显示的标注 state 和 每个 action 的类型就可以了。

TODO

以上的尝试,大致满足了编写 model 时候 state 与 reducers 类型的贯通;但在实际应用中,我们需要也能做到更全面的类型贯通,比如:

  • effect 逻辑里 call、put 的类型化
  • 由 reducers、effects 映射生成的 action 的类型化
  • 不仅仅局限于 redux model,还有 hooks model
  • 不仅仅是 generator effects,还有 async & await effects

下一节里,将详尽的介绍 tkit-model [@tefe/model] 是如何实现类型安全的。