原发于知乎专栏,欢迎关注:
自从 Redux
诞生后,函数式编程在前端一直很热;去年7月,Typescript
发布 2.0,OOP 数据流框架也开始火热,社区更倾向于类型友好、没有 Redux 那么冗长烦琐的 Mobx 和 。
然而静态类型并没有绑定 OOP。随着 Redux 社区对 TS 的拥抱以及 TS 自身的发展,TS 对 FP 的表达能力势必也会越来越强。Redux 社区也需要群策群力,为 TS 和 FP 的伟大结合做贡献。
本文主要介绍 Typescript
一些有意思的高级特性;并用这些特性对 Redux 做了类型优化,例如:推导全局的 Redux State 类型、Reducer 每个 case 下拿到不同的 payload 类型;Redux 去形式化与 Typescript 的结合;最后介绍了一些 React 中常用的 Typescript 技巧。
理论基础
Mapped Types
在 Javascript
中,字面量对象和数组是非常强大灵活。引进类型后,如何避免因为类型的约束而使字面量对象和数组死气沉沉,Typescript
灵活的 interface 是一个伟大的发明。
下面介绍的 让 interface 更加强大。大家在 js 中都用过 map 运算。在 TS 中,interface 也能做 map 运算。
// 将每个属性变成可选的。type Optional= { [key in keyof T]?: T[key];}复制代码
从字面量对象值推导出 interface 类型,并做 map 运算:
type NumberMap= { [key in keyof T]: number;}function toNumber (obj: T): NumberMap { return Object.keys(obj).reduce((result, key) => { return { ...result, [key]: Number(result[key]), }; }, {}) as any;}const obj2 = toNumber({ a: '32', b: '64',});复制代码
在 interface map 运算的支持下,obj2 能推导出精准的类型。
获取函数返回值类型
在 TS 中,有些类型是一个类型集,比如 interface,function。TS 能够通过一些方式获取类型集的子类型。比如:
interface Person { name: string;}// 获取子类型const personName: Person['name'];复制代码
然而,对于函数子类型,TS 暂时没有直接的支持。不过江湖上有一种类型推断的方法,可以获取返回值类型。
虽然该方法可以说又绕又不够优雅,但是函数返回值类型的推导,能够更好地支持函数式编程,收益远大于成本。
type Reverse= (arg: any) => T;function returnResultType (arg: Reverse ): T { return {} as any as T;}// result 类型是 numberconst result = returnResultType((arg: any) => 3);type ResultType = typeof result;复制代码
举个例子,当我们在写 React-redux connect 的时候,返回结构极有可能与 state 结构不尽相同。而通过推导函数返回类型的方法,可以拿到准确的返回值类型:
type MapProps= (state?: GlobalState, ownProps?: any) => NewState;function returnType (mapStateToProps: MapProps ) { return {} as any as NewState;}复制代码
使用方法:
function mapStateToProps(state?: GlobalState, ownProp?: any) { return { ...state.dataSrc, a: '', };};const mockNewState = returnType(mapStateToProps);type NewState = typeof mockNewState;复制代码
可辨识联合(Discriminated Unions)
关于 Discriminated Unions
,官方文档已有详细讲解,本文不再赘述。链接如下:
可辨识联合是什么,我只引用官方文档代码片段做快速介绍:
interface Square { kind: "square"; size: number;}interface Rectangle { kind: "rectangle"; width: number; height: number;}type Shape = Square | Rectangle;function area(s: Shape) { switch (s.kind) { // 在此 case 中,变量 s 的类型为 Square case "square": return s.size * s.size; // 在此 case 中,变量 s 的类型为 Rectangle case "rectangle": return s.height * s.width; }}复制代码
在不同的 case 下,变量 s 能够拥有不同的类型。我想读者一下子就联想到 Reducer 函数了吧。注意 interface 中定义的 kind 属性的类型,它是一个字符串字面量类型。
redux 类型优化
combineReducer 优化
原来的定义:
type Reducer= (state: S, action: any) => S;function combineReducers(reducers: ReducersMapObject): Reducer;复制代码
粗看这个定义,好似没有问题。但熟悉 Redux 的读者都知道,该定义忽略了 ReducersMapObject
和 S 的逻辑关系,S 的结构是由 ReducersMapObject
的结构决定的。
如下所示,先用 Mapped Types
拿到 ReducersMapObject
的结构,然后用获取函数返回值类型的方法拿到子 State
的类型,最后拼成一个大 State
类型。
type Reducer= (state: S, action: any) => S;type ReducersMap= { [key in keyof FullState]: Reducer ;}function combineReducers (reducersMap: ReducersMap ): Reducer ;复制代码
使用新的 combineReducers 类型覆盖原先的类型定义后,经过 combineReducers 的层层递归,最终可以通过 RootReducer 推导出 Redux 全局 State 的类型!这样在 Redux Thunk 中和 connect 中,可以享受全局 State 类型,再也不需要害怕写错局部 state 路径了!
拿到全局 State 类型:
function returnType(reducersMap: ReducersMap ): FullState { return ({} as any) as FullState;}const mockGlobalState = returnType(RootReducer);type GlobalState = typeof mockGlobalState;type GetState = () => GlobalState;复制代码
去形式化 & 类型推导
Redux 社区一直有很多去形式化的工具。但是现在风口不一样了,去形式化多了一项重大任务,做好类型支持!
关于类型和去形式化,由于 Redux ActionCreator 的型别取决于实际项目使用的 Redux 异步中间件。因此本文抛开笔者自身业务场景,只谈方法论,只做最简单的 ActionCreator 解决方案。读者可以用这些方法论创建适合自己项目的类型系统。
经团队同学提醒,为了读者有更好的类型体感,笔者创建了一个 repo 供读者体验:
读者可以 clone 下来在 vscode 中进行体验。
Redux Type
用 enum
来声明 Redux Type
,可以说是最精简的了。
enum BasicTypes { changeInputValue, toggleDialogVisible,}const Types = createTypes(prefix, BasicTypes);复制代码
然后用 createTypes
函数修正 enum
的类型和值。
createTypes
的定义如下所示,一方面用 Proxy 对属性值进行修正。另一方面用 Mapped Types
对类型进行修正。
type ReturnTypes= { [key in keyof EnumTypes]: key;}function createTypes (prefix, enumTypes: EnumTypes): ReturnTypes { return new Proxy(enumTypes as any, { get(target, property: any) { return prefix + '/' + property; } })}复制代码
读者请注意,ReturnTypes 中,Redux Type
类型被修正为一个字符串字面量类型(key)!以为创造一个可辨识联合做准备。
Redux Action 类型优化
市面上有很多 Redux 的去形式化工具,因此本文不再赘述 Redux Action
的去形式化,只说 Redux Action 的类型优化。
笔者总结如下3点:
- 1、要有一个整体 ActionCreators 的 interface 类型。
例如,可以定义定一个字面量对象来存储 actionCreators。
const actions = { /** 加 */ add: ... /** 乘以 */ multiply: ...}复制代码
一方面其它模块引用起来会很方便,一方面可以对字面量做批量类型推导。并且其中的注释,只有在这种字面量下,才能够在 vscode 中解析,以在其它模块引用时可以提高辨识度,提高开发体验。
- 2、每一个 actionCreator 需要定义 payload 类型。
如下代码所示,无论 actionCreator 是如何创建的,其 payload 类型必须明确指定。以便在 Reducer 中享用 payload 类型。
const actions = { /** 加 */ add() { return { type: Types.add, payload: 3 }; }, /** 乘以 */ multiply: createAction<{ num: number }>(Types.multiply)}复制代码
- 3、推导出可辨识联合类型。
最后,还要能够通过 actions 推导出可辨识联合类型。如此才能在 Reducer 不同 case 下享用不同的 payload 类型。
需要推导出的 ActionType 结构如下:
type ActionType = { type: 'add', payload: number } | { type: 'multiply', payload: { num: number } };复制代码
推导过程如下:
type ActionCreatorMap= { [key in keyof ActionMap]: (payload?, arg2?, arg3?, arg4?) => ActionMap[key]};type ValueOf = ActionMap[keyof ActionMap];function returnType (actions: ActionCreatorMap ) { type Action = ValueOf ; return {} as any as Action;}const mockAction = returnType(actions);type ActionType = typeof mockAction;function reducer(state: State, action: ActionType): State { switch (action.type) { case Types.add: { return ... } case Types.muliple: { return ... } }}复制代码
前端类型优化
常用的React类型
- Event
React 中 Event 参数很常见,因此 React 提供了丰富的关于 Event 的类型。比如最常用的 React.ChangeEvent:
// HTMLInputElement 为触发 Event 的元素类型handleChange(e: React.ChangeEvent) { // e.target.value // e.stopPropagation}复制代码
笔者更喜欢把 Event 转换成对应的 value
function pipeEvent(func: any) { return (event: React.ChangeEvent ) => { return func(event.target.value, event); };} 复制代码
- RouteComponentProps
ReactRoute 提供了 RouteComponentProps 类型,提供了 location、params 的类型定义
type Props = OriginProps & RouteComponentProps复制代码
自动产生接口类型
一般来说,前后端之间会用一个 API 约定平台或者接口约定文档,来做前后端解耦,比如 rap、 swagger。笔者在团队中做了一个把接口约定转换成 Typescript 类型定义代码的。经过笔者团队的实践,这种工具对开发效率、维护性都有很大的提高。
接口类型定义对开发的帮助:
在可维护性上。例如,一旦接口约定进行更改,API 的类型定义代码会重新生成,Typescript 能够检测到字段的不匹配,前端便能快速修正代码。最重要的是,由于前端代码与接口约定的绑定关系,保证了接口约定文档具有百分百的可靠性。我们得以通过接口约定来构建一个可靠的测试系统,进行自动化的联调与测试。
常用的默认类型
- Partial
把 interface 所有属性变成可选:
interface Obj { a: number; b: string;}type OptionalObj = Partial// interface OptionalObj { // a?: number;// b?: string;// }复制代码
- Readonly
把 interface 所有属性变成 readonly:
interface Obj { a: number; b: string;}type ReadonlyObj = Readonly// interface ReadonlyObj { // readonly a: number;// readonly b: string;// }复制代码
- Pick
interface T { a: string; b: number; c: boolean;}type OnlyAB = Pick;// interface OnlyAB { // a: string;// b: number;// }复制代码
总结
在 FP 中,函数就像一个个管道,在管道的连接处的数据块的类型总是不尽相同。下一层管道使用类型往往需要重新定义。
但是如果有一个确定的推导函数返回值类型的方法,那么只需要知道管道最开始的数据块类型,那么所有管道连接处的类型都可以推导出来。
当前 TS 版本尚不支持直接获取函数返回值类型,虽然本文介绍的间接方法也能解决问题,但最好还是希望 TS 早日直接支持:。
FP 就像一匹脱缰的野马,请用类型拴住它。