0%

TypeScript高级类型

高级类型

TypeScript为了保障语言的灵活性,而引入的一些语言特效。这些特性有助于开发者应对复杂,多变的开发场景。

交叉类型

交叉类型是将多个类型合并为一个类型。 新的类型拥有所有类型的特性。我们大多是在对象混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在 JavaScript 里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
interface DogInterface {
run(): void
}
interface CatInterface {
jump(): void
}

// 交叉类型
let pet: DogInterface & CatInterface = {
run() {},
jump() {}
}

联合类型

变量的类型并不确定,可以为多个类型中的一个。

1
let a: number | string = 1

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Dog implements DogInterface {
run() { }
eat() { }
}
class Cat implements CatInterface {
jump() { }
eat() { }
}
enum Master { Boy, Girl }
function getPet(master: Master) {
let pet = master === Master.Boy ? new Dog() : new Cat();
// 类型“Dog | Cat”上不存在属性“run”。类型“Cat”上不存在属性“run”。
// pet.run()
// 类型“Dog | Cat”上不存在属性“jump”。类型“Dog”上不存在属性“jump”。
// pet.jump()
// 类型保护
if (pet instanceof Dog){
pet.run()
}else{
pet.jump()
}
pet.eat()
return pet
}

这里的联合类型可能有点复杂:如果一个值的类型是 A | B,我们能够确定的是它包含了 AB 中共有的成员。这个例子里,Dog 具有一个 run 方法,我们不能确定一个 Cat | Dog 类型的变量是否有 jump 方法。 如果变量在运行时是 Dog 类型,那么调用 pet.jump() 就出错了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
}
}
console.log(area({kind: 'circle', radius: 1}))

上面的方法,如果遗漏了 circle 计算面试的实现逻辑,程序是不会报错的。如果想用TypeScript约束这种错误,进行错误提示可以使用下面的方法:

  • 设置函数返回值类型
  • 设置never类型,设置default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 设置函数返回值类型 "strictNullChecks": true
// 返回undefined会报错
function area(s: Shape): number {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
}
}

// 设置default
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case 'circle':
return Math.PI * s.radius ** 2
default:
// 检测s是不是never类型,不是never类型,说明前面的分支判断有遗漏,会报错
return ((e: never) => {throw new Error(e)})(s)
}
}

字面量类型

有些时候不仅需要限定变量的类型,而且还需要限定变量的取值在某个范围之类。

1
2
3
// 字面量联合类型
let b: 'a' | 'b' | 'c'
let c: 1 | 2 | 3

索引类型

索引类型会使用 keyof T, 索引类型查询操作符。

使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。

1
2
3
4
5
6
7
8
9
10
let obj = {
name: '11',
age: 22
}

function getValues(obj: any, keys: string[]) {
return keys.map(key => obj[key])
}

console.log(getValues(obj, ['sex']))

在上面的代码中 obj 对象没有 sex 属性,TypeScript没有报错,如果想TypeScript进行相关报错提示,可以使用索引类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getValues<T, K extends keyof T>(o: T, keys: K[]): T[K][] {
return keys.map(n => o[n]);
}

interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = getValues(person, ['name']); // ok, string[]

上面的代码使用 泛型变量 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
2
3
4
interface PersonPartial {
name?: string;
age?: number;
}

或者我们想要一个只读版本:

1
2
3
4
interface PersonReadonly {
readonly name: string;
readonly age: number;
}

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly类型或可选的。 下面是一些例子:

1
2
3
4
// 只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
1
2
3
4
// 可选
type Partial<T> = {
[P in keyof T]?: T[P];
}

像下面这样使用:

1
2
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

下面来看看最简单的映射类型和它的组成部分:

1
2
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

类型变量 K,它会依次绑定到每个属性。
字符串字面量联合的Keys,它包含了要迭代的属性名的集合。
属性的结果类型。
在个简单的例子里, Keys是硬编码的的属性名列表并且属性类型永远是 boolean,因此这个映射类型等同于:

1
2
3
4
type Flags = {
option1: boolean;
option2: boolean;
}

常见的映射类型

  • Readonly:只读
  • Partial:可选
  • Pick:抽取子集
  • Record
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Obj {
a: string;
b: number;
}
// 只读
type ReadonlyObj = Readonly<Obj>

// 可选
type PartialObj = Partial<Obj>

// 抽取obj子集
type PickObj = Pick<Obj, 'a' | 'b'>

type RecordObj = Record<'x' | 'y', Obj>

ReadonlyPartialPick同态 的,意思是只作用于obj属性而不会引入新的属性。

Record 映射会引入新属性,属于 非同态 映射类型。

Readonly的实现原理

1
2
3
4
5
6
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

从源码可以看出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
2
3
4
5
6
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

可选和只读映射类型的实现原理几乎一样,知识把所有属性变为可选

Pick映射类型的实现原理

1
2
3
4
5
6
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

Pick映射类型有两个参数:

  • 第一个参数T,表示要抽取的目标对象
  • 第二个参数K,具有一个约束:K一定要来自T所有属性字面量的联合类型,
  • 即映射得到的新类型的属性一定要从K中选取

Record映射类型的实现原理

1
2
3
4
5
6
7
8
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

type RecordObj = Record<'x' | 'y', Obj>

第一个参数是预定义的新属性,比如x,y。属性不来自Obj。

第二个参数就是已知类型Obj。

映射出的新类型所具有的属性由Record的第一个属性指定

而这些属性类型为第二个参数指定的已知类型

总结:映射类型本质上,是预先定义的泛型接口,通常还会结合索引类型,获取对象的属性和属性值,从而将一个对象映射成我们想要的结构

条件类型

条件类型是一种由条件表达式所决定的类型。

条件类型使类型具有了不唯一性,同样增加了语言的灵活性。

表现形式

1
T extends U ? X : Y

若类型T可被赋值给类型U,那么结果类型就是X类型,否则就是Y类型

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 条件类型
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'Function' :
'object'

// 定义类型T1为条件类型,传入参数string,指定t1为string类型
type T1 = TypeName<string>
// 定义类型T2为条件类型,传入参数string[]
type T2 = TypeName<string[]>

分步式条件类型

当类型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
2
// 如果T可以被赋值给U,结果类型为never类型,否则为T类型
type Diff<T, U> = T extends 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
2
// 如果T可以被赋值给U,结果类型为never类型,否则为T类型
type Diff<T, U> = T extends U ? never : T

可以基于 Diff 类型从T中移除不需要的类型,如undefined和null

1
2
3
4
5
// Diff扩展:从T中过滤掉undefined和null
type NotNull<T> = Diff<T, undefined | null>

// 过滤掉undefined和null,T5的类型就变成了string和number
type T5 = NotNull<string | number | undefined | null>

T5 会过滤 undefinednull 类型,推断为 string | number 联合类型。

上边实现的 DiffNotNull 类型,Ts库内置类型已经实现了相关功能。

1
2
Diff的内置类型叫做Exclude<T, U>
NotNull的内置类型叫做NonNullable<T>

此外,官方还预置了一些条件类型,如: Extract<T, U>Exclude<T, U>ReturnType<T>

1
2
3
4
Extract和Exclude相反
Exclude:是从类型T中过滤掉可以赋值给类型U的类型
Extract:可以从类型T中抽取出可以赋值给U的类型
ReturnType:接受一个函数类型,并返回这个函数的返回值类型。
1
2
3
4
5
type T6 = Extract<'a' | 'b' | 'c', 'a' | 'e'>
type T7 = Exclude<'a' | 'b' | 'c', 'a' | 'e'>
type T8 = ReturnType<() => string>

type Extract<T, U> = T extends U ? T : never;

类型推断 T6"a",类型。
类型推断 T7'b' | 'c' 联合类型。
类型推断 T8string 类型。

ReturnType源码分析

1
2
3
4
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
1
2
3
4
5
6
7
T extends (...args: any) => any:
ReturnType要求参数T可以赋值给一个函数,这个函数有任意的参数,返回值类型也是任意的
由于函数返回值类型不确定,这里使用了infer关键字,表示待推断,延迟推断,需要根据实际的情况确定

infer R ? R : any:
(infer R),返回值类型 R 此时是不能确定的,只有在函数执行之后才知道,是一种延迟推断,所以用 infer 修饰。
如果实际类型是R,那么结果类型就是R,否则返回值类型就是any

参考

-------------本文结束感谢您的阅读-------------