自从 Redux
诞生后,函数式编程在前端一直很热;去年7月,Typescript
发布 2.0,OOP 数据流框架也开始火热,社区更倾向于类型友好、没有 Redux 那么冗长烦琐的 Mobx 和 dob。
然而静态类型并没有绑定 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 是一个伟大的发明。
下面介绍的 Mapped Types
让 interface 更加强大。大家在 js 中都用过 map 运算。在 TS 中,interface 也能做 map 运算。
// 将每个属性变成可选的。
type Optional<T> = {
[key in keyof T]?: T[key];
}
从字面量对象值推导出 interface 类型,并做 map 运算:
type NumberMap<T> = {
[key in keyof T]: number;
}
function toNumber<T>(obj: T): NumberMap<T> {
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<T> = (arg: any) => T;
function returnResultType<T>(arg: Reverse<T>): T {
return {} as any as T;
}
// result 类型是 number
const result = returnResultType((arg: any) => 3);
type ResultType = typeof result;
举个例子,当我们在写 React-redux connect 的时候,返回结构极有可能与 state 结构不尽相同。而通过推导函数返回类型的方法,可以拿到准确的返回值类型:
type MapProps<NewState> = (state?: GlobalState, ownProps?: any) => NewState;
function returnType<NewState>(mapStateToProps: MapProps<NewState>) {
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<S> = (state: S, action: any) => S;
function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
粗看这个定义,好似没有问题。但熟悉 Redux 的读者都知道,该定义忽略了 ReducersMapObject
和 S 的逻辑关系,S 的结构是由 ReducersMapObject
的结构决定的。
如下所示,先用 Mapped Types
拿到 ReducersMapObject
的结构,然后用获取函数返回值类型的方法拿到子 State
的类型,最后拼成一个大 State
类型。
type Reducer<S> = (state: S, action: any) => S;
type ReducersMap<FullState> = {
[key in keyof FullState]: Reducer<FullState[key]>;
}
function combineReducers<FullState>(reducersMap: ReducersMap<FullState>): Reducer<FullState>;
使用新的 combineReducers 类型覆盖原先的类型定义后,经过 combineReducers 的层层递归,最终可以通过 RootReducer 推导出 Redux 全局 State 的类型!这样在 Redux Thunk 中和 connect 中,可以享受全局 State 类型,再也不需要害怕写错局部 state 路径了!
拿到全局 State 类型:
function returnType<FullState>(reducersMap: ReducersMap<FullState>): FullState {
return ({} as any) as FullState;
}
const mockGlobalState = returnType(RootReducer);
type GlobalState = typeof mockGlobalState;
type GetState = () => GlobalState;
去形式化 & 类型推导
Redux 社区一直有很多去形式化的工具。但是现在风口不一样了,去形式化多了一项重大任务,做好类型支持!
关于类型和去形式化,由于 Redux ActionCreator 的型别取决于实际项目使用的 Redux 异步中间件。因此本文抛开笔者自身业务场景,只谈方法论,只做最简单的 ActionCreator 解决方案。读者可以用这些方法论创建适合自己项目的类型系统。
经团队同学提醒,为了读者有更好的类型体感,笔者创建了一个 repo 供读者体验:
https://github.com/jasonHzq/redux-ts-helper
读者可以 clone 下来在 vscode 中进行体验。
Redux Type
用 enum
来声明 Redux Type
,可以说是最精简的了。
enum BasicTypes {
changeInputValue,
toggleDialogVisible,
}
const Types = createTypes(prefix, BasicTypes);
然后用 createTypes
函数修正 enum
的类型和值。
createTypes
的定义如下所示,一方面用 Proxy 对属性值进行修正。另一方面用 Mapped Types
对类型进行修正。
type ReturnTypes<EnumTypes> = {
[key in keyof EnumTypes]: key;
}
function createTypes<EnumTypes>(prefix, enumTypes: EnumTypes): ReturnTypes<EnumTypes> {
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<ActionMap> = {
[key in keyof ActionMap]: (payload?, arg2?, arg3?, arg4?) => ActionMap[key]
};
type ValueOf<ActionMap> = ActionMap[keyof ActionMap];
function returnType<ActionMap>(actions: ActionCreatorMap<ActionMap>) {
type Action = ValueOf<ActionMap>;
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<HTMLInputElement>) {
// e.target.value
// e.stopPropagation
}
笔者更喜欢把 Event 转换成对应的 value
function pipeEvent<Element = HTMLInputElement>(func: any) {
return (event: React.ChangeEvent<HTMLInputElement>) => {
return func(event.target.value, event);
};
}
<input onChange={pipeEvent(actions.changeValue)}>
- RouteComponentProps
ReactRoute 提供了 RouteComponentProps 类型,提供了 location、params 的类型定义
type Props = OriginProps & RouteComponentProps<Params, {}>
自动产生接口类型
一般来说,前后端之间会用一个 API 约定平台或者接口约定文档,来做前后端解耦,比如 rap、 swagger。笔者在团队中做了一个把接口约定转换成 Typescript 类型定义代码的。经过笔者团队的实践,这种工具对开发效率、维护性都有很大的提高。
接口类型定义对开发的帮助:
在可维护性上。例如,一旦接口约定进行更改,API 的类型定义代码会重新生成,Typescript 能够检测到字段的不匹配,前端便能快速修正代码。最重要的是,由于前端代码与接口约定的绑定关系,保证了接口约定文档具有百分百的可靠性。我们得以通过接口约定来构建一个可靠的测试系统,进行自动化的联调与测试。
常用的默认类型
- Partial
把 interface 所有属性变成可选:
interface Obj {
a: number;
b: string;
}
type OptionalObj = Partial<Obj>
// interface OptionalObj {
// a?: number;
// b?: string;
// }
- Readonly
把 interface 所有属性变成 readonly:
interface Obj {
a: number;
b: string;
}
type ReadonlyObj = Readonly<Obj>
// interface ReadonlyObj {
// readonly a: number;
// readonly b: string;
// }
- Pick
interface T {
a: string;
b: number;
c: boolean;
}
type OnlyAB = Pick<T, 'a' | 'b'>;
// interface OnlyAB {
// a: string;
// b: number;
// }
总结
在 FP 中,函数就像一个个管道,在管道的连接处的数据块的类型总是不尽相同。下一层管道使用类型往往需要重新定义。
但是如果有一个确定的推导函数返回值类型的方法,那么只需要知道管道最开始的数据块类型,那么所有管道连接处的类型都可以推导出来。
当前 TS 版本尚不支持直接获取函数返回值类型,虽然本文介绍的间接方法也能解决问题,但最好还是希望 TS 早日直接支持:issue。
FP 就像一匹脱缰的野马,请用类型拴住它。