博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
你所不知道的 Typescript 与 Redux 类型优化
阅读量:7009 次
发布时间:2019-06-28

本文共 7639 字,大约阅读时间需要 25 分钟。

原发于知乎专栏,欢迎关注:

自从 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 就像一匹脱缰的野马,请用类型拴住它。

转载地址:http://lgttl.baihongyu.com/

你可能感兴趣的文章
collection的框架结构
查看>>
c++中的对象复制
查看>>
ubuntu下linux内核源码阅读工具和调试方法总结
查看>>
PHP生成UTF-8编码的CSV文件用Excel打开乱码的解决办法
查看>>
xshell快捷键
查看>>
win10无法使用内置管理员账户打开应用怎么办
查看>>
Fidller证书安装失败的解决--creation of the root certificate was not successful
查看>>
Vue2---父子组件之间的访问
查看>>
重新想象 Windows 8 Store Apps (41) - 打印
查看>>
100%布局的头部,内部内容960居中出现滚动条的时候,注意的小东西
查看>>
Ubuntu系统里下载安装配置redis-2.2.13.tar.gz
查看>>
2017年PHP程序员未来路在何方
查看>>
vue - webpack.base.conf.js
查看>>
MongoDB 主从复制小实验
查看>>
Linux Shell常用技巧(七)
查看>>
iOS网络编程解析协议二:XML数据传输解析
查看>>
Leetcode: Concatenated Words
查看>>
Python 多线程
查看>>
oracle数据库性能
查看>>
关于VS中的调试信息输出
查看>>