高级类型
TypeScript为了保障语言的灵活性,而引入的一些语言特效。这些特性有助于开发者应对复杂,多变的开发场景。
交叉类型
交叉类型是将多个类型合并为一个类型。 新的类型拥有所有类型的特性。我们大多是在对象混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在 JavaScript 里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:
1 | interface DogInterface { |
联合类型
变量的类型并不确定,可以为多个类型中的一个。
1 | let a: number | string = 1 |
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
1 | class Dog implements DogInterface { |
这里的联合类型可能有点复杂:如果一个值的类型是 A | B
,我们能够确定的是它包含了 A
和 B
中共有的成员。这个例子里,Dog
具有一个 run
方法,我们不能确定一个 Cat | Dog
类型的变量是否有 jump
方法。 如果变量在运行时是 Dog
类型,那么调用 pet.jump()
就出错了。
1 | interface Square { |
上面的方法,如果遗漏了 circle
计算面试的实现逻辑,程序是不会报错的。如果想用TypeScript约束这种错误,进行错误提示可以使用下面的方法:
- 设置函数返回值类型
- 设置never类型,设置default
1 | // 设置函数返回值类型 "strictNullChecks": true |
字面量类型
有些时候不仅需要限定变量的类型,而且还需要限定变量的取值在某个范围之类。
1 | // 字面量联合类型 |
索引类型
索引类型会使用 keyof T
, 索引类型查询操作符。
使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。
1 | let obj = { |
在上面的代码中 obj
对象没有 sex
属性,TypeScript没有报错,如果想TypeScript进行相关报错提示,可以使用索引类型。
1 | function getValues<T, K extends keyof T>(o: T, keys: K[]): T[K][] { |
上面的代码使用 泛型变量 T
约束 obj对象
,使用泛型变量 K
约束 keys数组
,并给 K
增加 类型约束
,让它继承 obj
所有属性的联合类型
。函数的返回值是一个数组,数组的元素的类型就是 obj对象
的属性 K
对应的类型。
编译器会检查 name
是否真的是 obj的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T
, 索引类型查询操作符。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。 例如:
1 | let personProps: keyof Person; // 'name' | 'age' |
第二个操作符是 T[K], 索引访问操作符。
映射类型
一个常见的需求是将一个已知的类型每个属性都变为可选的:
1 | interface PersonPartial { |
或者我们想要一个只读版本:
1 | interface PersonReadonly { |
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型
。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly类型或可选的。 下面是一些例子:
1 | // 只读 |
1 | // 可选 |
像下面这样使用:
1 | type PersonPartial = Partial<Person>; |
下面来看看最简单的映射类型和它的组成部分:
1 | type Keys = 'option1' | 'option2'; |
它的语法与索引签名的语法类型,内部使用了 for .. in
。 具有三个部分:
类型变量 K
,它会依次绑定到每个属性。
字符串字面量联合的Keys
,它包含了要迭代的属性名的集合。
属性的结果类型。
在个简单的例子里, Keys
是硬编码的的属性名列表并且属性类型永远是 boolean
,因此这个映射类型等同于:
1 | type Flags = { |
常见的映射类型
- Readonly:只读
- Partial:可选
- Pick:抽取子集
- Record
1 | interface Obj { |
Readonly
, Partial
和 Pick
是 同态
的,意思是只作用于obj属性而不会引入新的属性。
Record
映射会引入新属性,属于 非同态
映射类型。
Readonly的实现原理
1 | /** |
从源码可以看出Readonly是一个可索引类型的泛型接口
- 1)索引签名为P in keyof T :其中keyof T,表示类型T所有属性的联合类型
- 2)P in :相当于执行了一个for in操作,会把变量P依次绑定到T的所有属性上
- 3)索引签名的返回值就是一个索引访问操作符 : T[P] 这里代表属性P所指定的类型
- 4)最后再加上Readonly就把所有的属性变成了只读,这就是Readonly的实现原理
Partial的实现原理
1 | /** |
可选和只读映射类型的实现原理几乎一样,知识把所有属性变为可选
Pick映射类型的实现原理
1 | /** |
Pick映射类型有两个参数:
- 第一个参数T,表示要抽取的目标对象
- 第二个参数K,具有一个约束:K一定要来自T所有属性字面量的联合类型,
- 即映射得到的新类型的属性一定要从K中选取
Record映射类型的实现原理
1 | /** |
第一个参数是预定义的新属性,比如x,y。属性不来自Obj。
第二个参数就是已知类型Obj。
映射出的新类型所具有的属性由Record的第一个属性指定
而这些属性类型为第二个参数指定的已知类型
总结:映射类型本质上,是预先定义的泛型接口,通常还会结合索引类型,获取对象的属性和属性值,从而将一个对象映射成我们想要的结构
。
条件类型
条件类型是一种由条件表达式所决定的类型。
条件类型使类型具有了不唯一性,同样增加了语言的灵活性。
表现形式
1 | T extends U ? X : Y |
若类型T可被赋值给类型U,那么结果类型就是X类型,否则就是Y类型
简单示例
1 | // 条件类型 |
分步式条件类型
当类型T为联合类型时:
T为类型A和类型B的联合类型,结果类型会变成多个条件类型的联合类型,如下:
1 | (A | B) extends U ? X : Y |
可以进行拆解:
1 | (A extends U ? X : Y) | (B extends U ? X : Y) |
这时定义的变量就会被推断为联合类型。
1 | type T3 = TypeName<string | string[]> |
传入 string | string[]
联合类型,T3被推断为 "string" | "object"
的联合类型。
类型过滤
利用分步式条件类型以实现对类型的过滤
定义一个类型 Diff
:
1 | // 如果T可以被赋值给U,结果类型为never类型,否则为T类型 |
定义一个类型 T4
:
1 | type T4 = Diff<'a' | 'b' | 'c', 'a' | 'e'> |
T4
被推断了 "b" | "c"
联合类型。
按照 分步式条件类型
拆解逻辑分析:
- 1)首先 Diff被拆解为多个条件类型的联合类型:
Diff<"a", "a" | "e"> | Diff<"b", "a" | "e"> | Diff<"c", "a" | "e">
- 2)进行条件类型判断
"a"
可以被赋值给字面量联合类型"a" | "e"
,返回never。"b"
不可以被赋值给字面量联合类型"a" | "e"
,返回"b"
。"c"
不可以被赋值给字面量联合类型"a" | "e"
,返回"c"
- 返回
never | "b" | "c"
- 3)最后,never和b,c的联合类型为’b’ | ‘c’
Diff
类型作用:
可以从类型T中过滤掉可以被赋值给类型U的类型
。
基于 Diff 类型进行扩展
1 | // 如果T可以被赋值给U,结果类型为never类型,否则为T类型 |
可以基于 Diff
类型从T中移除不需要的类型,如undefined和null
1 | // Diff扩展:从T中过滤掉undefined和null |
T5
会过滤 undefined
和 null
类型,推断为 string | number
联合类型。
上边实现的 Diff
和 NotNull
类型,Ts库内置类型已经实现了相关功能。
1 | Diff的内置类型叫做Exclude<T, U> |
此外,官方还预置了一些条件类型,如: Extract<T, U>
和 Exclude<T, U>
和 ReturnType<T>
。
1 | Extract和Exclude相反 |
1 | type T6 = Extract<'a' | 'b' | 'c', 'a' | 'e'> |
类型推断 T6
为 "a"
,类型。
类型推断 T7
为 'b' | 'c'
联合类型。
类型推断 T8
为 string
类型。
ReturnType源码分析
1 | /** |
1 | T extends (...args: any) => any: |