Overview
到目前为止,用TS也写了不少业务了,大部分时候其实只要有 类
以及 类型
的概念,在用TS完成业务的过程中也就不会有太大的问题。或者有写 “C++” 或 “Java” 的经验,我们很快能从 “JS” 无痛切换到 TS。但是写久了就会发现,“TS” 最有意思的地方不是单纯的类型定义以及类型约束,或者说我们使用 “TS” 本身是为了达到类型约束的目的,类型定义只是我们的一个手段;它的 类型编程
才是在实际使用过程中最香的地方。所以我对 TS类型编程
做了一些探索。
TS 编译 JSX
在 TS + React
的项目里,我们通常会让TS的编译器 TSC
帮助我们完成JSX的转换,在tsconfig.json
的配置compilerOptions: { jsx: "" }
里,我们大概会有如下选择:
Mode | Input | Output | Output File Extension |
---|---|---|---|
preserve | <div /> | <div /> | .jsx |
react | <div /> | React.createElement("div") | .js |
react-native | <div /> | <div /> | .js |
react-jsx | <div /> | _jsx("div", {}, void 0) | .js |
这里主要列出两个常用的选项的区别:
preserve
这一模式会保留JSX标签,把TS编译成JS,因此我们编译完成后并不能直接运行,还需要另外对保留下来的jsx进行处理。如果是用Webpack
打包,则配置如下:rules: [ { test: /\.tsx?$/, use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-react"], }, }, { loader: "ts-loader", }, ], }, ],
react
这一模式会直接把jsx标签编译成React处理,最后得到的是可以直接运行的js文件,在Webpack
里只需要配置ts-loader
就行了,它会调用TSC
进行编译。
TS 类型编程
为了渐进式的理解TS的类型编程,我们首先需要了解它的一些类型操作符,然后在了解TS内置的一些Type utility
。基于这些我们再去探索如何使用TS实现更加灵活的类型工具。
泛型
泛型是TS里面类型编程的基础,它也是提高我们类型定义的复用性的基础。
function cloneObj<T>(arg: T): T {
return arg;
}
比如cloneObj就能被复用到各种对象的拷贝上,在类型定义里泛型可以看成是个插槽
;而在类型编程里,泛型就可以看成是变量
,我们可以再类型编程中使用这个变量和一些类型操作符去编写类型工具。
类型操作符
keyof
这个操作符主要是会取出对象类型里的所有key
然后得到一个联合类型
。type Animal = { name: string; age: number }; type AnumalKey = keyof Animal; // "name" | "age"
当我们取得对象类型里包含
string
或number
的索引签名时,keyof
也会把其中的string
和number
取出到union type
中。type Thing = { [key: number]: unknown } type ThingKey = keyof Thing; // number type MultiThing = { [key: string]: unknown } type MultiThingKey = keyof MultiThing; // string | number // 这里我们得到 string | number 是因为在js里对象的键会被默认转换成string,也就是说obj["1"]和obj[1]取得的结果是一样的。 type Obj = { name: string; [key: number]: unknown } type ObjKey = keyof Obj; // number | "name"
typeof
typeof
是推断一个变量的类型的操作符,在类型编程里,我们的输入就是类型。const foo = () => 'foo'; type Foo = typeof foo; // () => string type FooReturn = ReturnType<typeof foo>; // string
索引类型
就像在JS里我们能通过索引取得对应的值一样,在TS里我们也可以通过索引取得对应的类型,同时它也可以结合keyof
使用,效果更佳。type Animal = { name: string; age: number; fly: boolean } type Fly = Animal["fly"]; // boolean; type AnimalVal = Animal[keyof Animal]; // string | number | boolean
映射类型(Mapped Type)
映射类型是通过in
操作符遍历类型的key得到的新的类型。配合+
,-
操作符可以对readonly
,?
关键字进行增加或剔除处理。type Animal = { name: string; age: number } type Mapped = { [K in keyof Animal]: boolean; }; // { name: boolean; age; boolean }
我们对
Mapped
再抽象一下,加入泛型
,如下:type Mapped<T> = { [K in keyof T]: boolean; }
到这里是不是已经有
类型编程
那味了!
我们再来加入+
,-
操作符:type MappedToPartial<T> = { [K in keyof T]?: boolean } type MappedToRequired<T> = { [K in keyof T]-?: boolean; }
条件类型(? :)
同样和JS里的条件操作符一样,? :
也可以再TS里使用,只不过它的操作对象是类型。它通常和extends
一起使用,毕竟类型里是没有===, >, <
这些操作符去做判断逻辑的,extends
就起到了对应的作用。type Flatten<T> = T extends any[] ? T[number] : T; // Extracts out the element type. type Str = Flatten<string[]>; // string // Leaves the type alone. type Num = Flatten<number>; // number
还是继续说
Flatten
,假如我们这样写:Flatten<string[] | number[]>
,得到的结果是什么呢?到这我们不得不提到一个概念:类型分发
类型分发
对于联合类型
作为条件类型
的操作数时,当联合类型单个的被用为裸类型
时,TS会把联合类型分发成一次一次的推断:type Example1 = Flatten<string[] | number[]>; // string | number
当联合类型被用为
包裹类型
时,TS不会对联合类型进行分发推断:type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; // 'StrOrNumArr' is no longer a union. type StrOrNumArr = ToArrayNonDist<string | number>; // (string | number)[] /* 我们要区分 string[] | number[] 和 (string | number)[]的区别 前者是指由 string数组 和 number数组 组成的联合类型 后者是指一个数组里可以有 string 和 number 两种元素 */
官方文档对
类型分发
的解释:Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.
infer
infer
是待推断的意思,在TS里它可以用来定义一个待推断类型变量。比如我们从一个redux的reducer里面提取state和action的类型:type ExtractReducer<T> = T extends (state: infer S, action: infer A) => infer S ? { state: S, action: A } : never; type RootType = ExtractReducer<typeof rootReducer>; type RootState = RootType["state"]; type RootAction = RootType["action"];
递归类型
类型编程
和普通的编程一样,也会有递归的场景,我们来看一个递归的例子:
DeepRequired
type DeepRequired<T extends object> = { [K in keyof T]-?: T[K] extends object ? DeepRequired<T[K]> : T[K]; } // 这样写看起来似乎已经完成了递归的逻辑,但是实际上是有问题的 // 实际上 T[K] = undefined | ..., T[K] extends object always be false // 所以我们需要做的是把字段K变成required,再做 T[K] extends object 判断,如下: type Required<T extends object> = { [K in keyof T]-?: T[K]; } type DeepRequired<T extends object> = { [K in keyof T]-?: Required<T>[K] extends object ? DeepRequired<T[K]> : T[K]; }
这些基本上就是我们在类型编程
中会用到的一些东西了,基于以上的这些操作符,结合递归思想,可以延伸出很多工具类型。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。