TypeScript 从入门到放弃(二):泛型、高级类型

前言

本文作为学习笔记,文中内容大多来自官方文档和一些资料,摘抄的部分会在文中标注出原文地址,可以直接参考原文。

上一篇学习了 TS 的基本数据类型、接口、函数和类等基本用法。接下来继续深入学习一些相对 JS 来说 TS 中新增的内容。

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

下面通过一个例子来了解泛型,如何实现一个打印函数呢?

1
2
3
4
function log (value: string): string {
console.log(value)
return value
}

上面的这个 log 方法,只能接收和返回 string 类型数据。如何才能让该方法接收和返回 string[] 类型的数据呢?

第一种方法:函数重载,通过重载可以动态的匹配符合的类型。

1
2
3
4
5
6
function log (value: string): string;
function log (value: string[]): string[]
function log (value: any): any {
console.log(value)
return value
}

第二种方法:联合类型

1
2
3
4
function log (value: string | string[]): string | string[] {
console.log(value)
return value
}

联合类型会比函数重载会简洁一点。如果前面学的比较扎实,相信会想到 any 类型。

第三种方法:any 类型

1
2
3
4
function log (value: any): any {
console.log(value)
return value
}

这种方式比联合类型和函数重载都简洁,但是存在一个问题会丢失信息,即传入的类型和返回的类型应该相同。

如何优雅的解决这个问题呢?就是泛型,它可以不指定参数和返回值的类型,只有在真正使用的时候才去确定。

1
2
3
4
5
6
function log<T> (value: T): T {
console.log(value)
return value
}
log<string>('a') // 类型可以省略
log(['a', 'b']) // 省略 <string[]>

这种方式是不是更简洁,这只是基本用法。

TS 中的高级类型也广泛使用了泛型。如,泛型定义函数别名、泛型接口等。

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 泛型 定义函数类型别名
type Log = <T>(value: T) => T //定义泛型别名
let myLog: Log = log

// 泛型在接口中使用
// 等价于 别名
// 也可以指定默认类型
interface ILog<T = string> { // 指定默认泛型类型。
(value: T): T;
}
// 指定类型。
let myILog: ILog<number> = log

泛型类和泛型约束

泛型类和泛型接口的写法差不多,在类名后面加 <>指定泛型类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 泛型类
class CLog<T> {
run (value: T) {
console.log(value)
return value
}
}
let clog1 = new CLog<number>()
clog1.run(10) // 10

let clog2 = new CLog()
clog2.run(['a', 'b']) // ['a','b']

类可以支持多种类型,增强程序的扩展性。还可以通过泛型约束,灵活控制类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 泛型约束
interface Length {
length: number
}
// 有时需要获取泛型中的一个属性,但是编译器不知道有没有 length 属性。
// 通过继承 Length 接口添加泛型约束。
function sLog<T extends Length> (value: T): T {
console.log(value, value.length)
return value
}

sLog([1]) // [1] 1
sLog('123') // 123 3
sLog({ length: 1 }) // {length: 1} 1

通过接口 Length 约束泛型。创建一个包含 .length 属性的接口,使用这个接口和 extends 关键字来实现约束。

小结

使用泛型有什么优点呢?

  • 可以动态支持类型,增强程序的扩展性。
  • 可以替代重载和联合类型声明,提高代码的简洁性。
  • 泛型约束,灵活控制类型间的约束。

高级类型

交叉类型

所谓的交叉类型是将多个类型合并为一个类型。将现有的多个类型叠加在一起,它包含所需的所有类型的特性。使用 & 符合。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
interface DogInterface {
run (): void
}
interface CatInterface {
jump (): void
}
// 定义对象实现交叉类型接口,run 和 jump 都必须实现。
let pet: DogInterface & CatInterface = {
run () { },
jump () {},
}

PS: 交叉类型取的是所有类型的并集,而不是交集。

联合类型

前面也学到了联合类型,联合类型取得是两者中的一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ADDog implements DogInterface {
run () { }
eat () {}
}
class ADCat implements CatInterface {
jump () { }
eat () {}
}

enum Master { Boy, Girl }
function getPet (master: Master) {
// 此时 pet 是联合类型
let pet = master === Master.Boy ? new ADDog() : new ADCat()
pet.eat()
// pet.run() // error
return pet
}

PS:联合类型取得所有类型的交集。这里取的 Dog | Cat 两者的交集。

可区分的联合类型

通过一个公共的字面量区分不同的类型。

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
26
27
28
29
30
31
32
33
34
35
interface Square {
kind: 'square', // 表示类型
size: number
}

interface Rectangle {
kind: 'rectangle',
width: number,
height: number
}

interface Circle {
kind: 'circle',
r: number
}

type Shape = Square | Rectangle | Circle

function area (s: Shape) {
switch (s.kind) { // 共有属性
case 'square': // 不同的类型保护区块。
return s.size * s.size
case 'rectangle':
return s.width * s.height
case 'circle':
return Math.PI * s.r ** 2
default:
// s为never类型,表示前面的分支都被覆盖。
// s不是never类型,说明前面分支有遗漏。
return ((e: never) => { throw new Error(e) })(s)
}
}

// 问题:新加kind时,存在问题。
console.log(area({ kind: 'circle', r: 1 })) // 输出: Undefined

索引类型

使用索引类型,编译器就能够检查使用了动态属性名的代码。

看一个 JS 例子。

1
2
3
4
5
6
7
8
9
10
11
let obj = {
a: 1,
b: 2,
c: 3
}
// 对象中选取属性值的子集
function getValues (obj: any, keys: string[]) {
return keys.map(key => obj[key])
}
console.log(getValues(obj, ['a', 'b'])) // [1, 2]
console.log(getValues(obj, ['e', 'f'])) // 并不会提示属性缺失。['undefined', 'undefined']

使用索引类型可以添加类型约束改造,需要使用 索引类型查询索引访问 操作符。

先看一下使用 TS 实现后结果。

1
2
3
4
5
6
7
// 泛型 T 约束 Obj;泛型 K 约束数组。
// K 继承自 T 所有属性的联合类型。
function getValuesTS<T, K extends keyof T> (o: T, names: K[]): T[K][] {
return names.map(key => o[key])
}
console.log(getValuesTS(obj, ['a', 'b']))
// console.log(getValuesTS(obj, ['e', 'f'])) // error, 编译器报错。

编译器会检查数组中的元素是否是 obj 的一个属性。需要注意的地方,**keyof T** 索引类型查询操作符。对任何类型的 T,keyof T 的结果为 T 上已知的公共属性名的联合。

1
2
3
4
5
6
// keyof T
interface Obj {
a: number,
b: string
}
let key: keyof Obj // key 是 a | b 类型的。

T[K] 索引访问操作符。作用就是 o[key] 具有的类型就是 obj['a'],在普通上下文中使用 T[K] 就像使用索引类型查询一样。

映射类型

有时需要将已知类型的每个属性变成可选的或者只读的。TS 提供类从旧类型中创建新类型的一种方式:映射类型

1
2
3
4
5
6
7
8
9
10
11
12
// 接口 Person
interface Person {
name: string;
age: number;
score: number
}
// 映射为只读类型
type ReadonlyPerson = Readonly<Person>
// 可选
type PartialPerson = Partial<Person>
// 选取部分类型
type PickPerson = Pick<Person, 'name' | 'age'>

Readonly<T>Partial<T>Partial<T> 是 TS 库内置的映射类型。还有很多其他的映射类型,以上三种类型:属性列表中的 keyof T 且结果类型是 T[P] 的变体。这种转换称为同态,映射只作用于 T 的属性而没有其它的。

小结

本小结学习了 TS 中提供几种高级类型,先熟悉概念和用法后期慢慢深入。

参考