TypeScript高级类型

https://www.cnblogs.com/tangshiwei/p/12052494.html

前言

对于有 JavaScript 基础的同学来说,入门 TypeScript 其实很容易,只需要简单掌握其基础的类型系统就可以逐步将 JS 应用过渡到 TS 应用。

1
2
3
4
// js
const double = (num) => 2 * num
// ts
const double = (num: number): number => 2 * num

然而,当应用越来越复杂,我们很容易把一些变量设置为 any 类型,TypeScript 写着写着也就成了 AnyScript。为了让大家能更加深入的了解 TypeScript 的类型系统,本文将重点介绍其高级类型,帮助大家摆脱 AnyScript。

泛型

在讲解高级类型之前,我们需要先简单理解泛型是什么。

泛型是强类型语言中比较重要的一个概念,合理的使用泛型可以提升代码的可复用性,让系统更加灵活。下面是维基百科对泛型的描述:

泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

泛型通过一对尖括号来表示( <> ),尖括号内的字符被称为 类型变量 ,这个变量用来表示类型。

1
2
3
4
5
6
7
8
9
function copy<T>(arg: T): T {
if (typeof arg === 'object') {
return JSON.parse(
JSON.stringify(arg)
)
} else {
return arg
}
}

这个类型 T,在没有调用 copy 函数的时候并不确定,只有调用 copy 的时候,我们才知道 T 具体代表什么类型。

1
const str = copy<string>('my name is typescript')

我们在 VS Code 中可以看到 copy 函数的参数以及返回值已经有了类型,也就是说我们调用 copy 函数的时候,给类型变量 T 赋值了 string。其实,我们在调用 copy 的时候可以省略尖括号,通过 TS 的类型推导是可以确定 T 为 string 的。

通用类型

TypeScript Notes
Keys keyof T
Values T[keyof T]
Readonly Readonly<T>
Exact T
Class typeof T
Subtype B extends A
Shape Partial<T> Makes all properties optional.
Required Required<T> Makes all properties required.
Return type ReturnType<T>, ReturnType<typeof someFunction>
Property type T[k]
Element type T[k]
Rest Exclude<A, B>
Difference / Omit Pick<A, Exclude<keyof A, B>> Omit is a type as of version 3.5
Pick Pick<T, T[k]> Useful when typing defaultProps (don't use Partial to type defaultProps). Example: `const defaultProps: Pick<ComponentProps, 'key1'

高级类型

除了 string、number、boolean 这种基础类型外,我们还应该了解一些类型声明中的一些高级用法。

全集和空集

any 类型,泛指一切可能的类型,对应全集。
never 类型对应空集。任何值,即使是 undefined 或 null 也不能赋值给 never 类型。

unknownany 的主要区别是 unknown 类型会更加严格:在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。

https://blog.csdn.net/weixin_33716557/article/details/93177689

交叉类型(&)

交叉类型说简单点就是将多个类型合并成一个类型,个人感觉叫做「合并类型」更合理一点,其语法规则和逻辑 “与” 的符号一致。

1
T & U

假如,我现在有两个类,一个按钮,一个超链接,现在我需要一个带有超链接的按钮,就可以使用交叉类型来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Button {
type: string
text: string
}

interface Link {
alt: string
href: string
}
type BL = Button & Link

const linkBtn: Button & Link = {
type: 'danger',
text: '跳转到百度',
alt: '跳转到百度',
href: 'http://www.baidu.com'
}

const linkBtn: BL = {
type: 'danger',
text: '跳转到百度',
alt: '跳转到百度',
href: 'http://www.baidu.com'
}

联合类型(|)

联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个。

1
T | U

例如,之前的 Button 组件,我们的 type 属性只能指定固定的几种字符串。

1
2
3
4
5
6
7
8
9
interface Button {
type: 'default' | 'primary' | 'danger'
text: string
}

const btn: Button = {
type: 'primary',
text: '按钮'
}

类型别名(type)

前面提到的交叉类型与联合类型如果有多个地方需要使用,就需要通过类型别名的方式,给这两种类型声明一个别名。类型别名与声明变量的语法类似,只需要把 constlet 换成 type 关键字即可。

type Alias = T | U

1
2
3
4
5
6
7
8
9
10
11
type InnerType = 'default' | 'primary' | 'danger'

interface Button {
type: InnerType
text: string
}

interface Alert {
type: ButtonType
text: string
}

类型索引(keyof)

keyof 类似于 Object.keys ,用于获取一个接口中 Key 的联合类型。

1
2
3
4
5
6
7
8
interface Button {
type: string
text: string
}

type ButtonKeys = keyof Button
// 等效于
type ButtonKeys = "type" | "text"

还是拿之前的 Button 类来举例,Button 的 type 类型来自于另一个类 ButtonTypes,按照之前的写法,每次 ButtonTypes 更新都需要修改 Button 类,如果我们使用 keyof 就不会有这个烦恼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface ButtonStyle {
color: string
background: string
}
interface ButtonTypes {
default: ButtonStyle
primary: ButtonStyle
danger: ButtonStyle
}
interface Button {
type: 'default' | 'primary' | 'danger'
text: string
}

// 使用 keyof 后,ButtonTypes修改后,type 类型会自动修改
interface Button {
type: keyof ButtonTypes
text: string
}

类型约束(extends)

这里的 extends 关键词不同于在 class 后使用 extends 的继承作用,泛型内使用的主要作用是对泛型加以约束。我们用我们前面写过的 copy 方法再举个例子:

1
2
3
4
5
6
7
type BaseType = string | number | boolean

// 这里表示 copy 的参数
// 只能是字符串、数字、布尔这几种基础类型
function copy<T extends BaseType>(arg: T): T {
return arg
}

如果我们传入一个对象就会有问题。

extends 经常与 keyof 一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 extendskeyof 进行约束。

1
2
3
4
5
6
function getValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key]
}

const obj = { a: 1 }
const a = getValue(obj, 'a')

这里的 getValue 方法就能根据传入的参数 obj 来约束 key 的值。

类型映射(in)

in 关键词的作用主要是做类的映射,遍历已有接口的 key 或者是遍历联合类型。下面使用内置的泛型接口 Readonly 来举例。

1
2
3
4
5
6
7
8
9
10
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

interface Obj {
a: string
b: string
}

type ReadOnlyObj = Readonly<Obj>

我们可以结构下这个逻辑,首先 keyof Obj 得到一个联合类型 'a' | 'b'

1
2
3
4
5
6
7
8
9
10
interface Obj {
a: string
b: string
}

type ObjKeys = 'a' | 'b'

type ReadOnlyObj = {
readonly [P in ObjKeys]: Obj[P];
}

然后 P in ObjKeys 相当于执行了一次 forEach 的逻辑,遍历 'a' | 'b'

1
2
3
4
type ReadOnlyObj = {
readonly a: Obj['a'];
readonly b: Obj['b'];
}

最后就可以得到一个新的接口。

1
2
3
4
interface ReadOnlyObj {
readonly a: string;
readonly b: string;
}

条件类型(U ? X : Y)

条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。

1
T extends U ? X : Y

上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y。下面使用内置的泛型接口 Extract 来举例。

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

如果 T 中的类型在 U 存在,则返回,否则抛弃。假设我们两个类,有三个公共的属性,可以通过 Extract 提取这三个公共属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Worker {
name: string
age: number
email: string
salary: number
}

interface Student {
name: string
age: number
email: string
grade: number
}


type CommonKeys = Extract<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'

infer 关键字

语句 let num 中,通过 let 来声明了一个变量,那怎样声明一个不确定的类型变量呢? 答案是使用 infer 关键字,infer R 就是声明了一个类型变量 R

在条件类型表达式中,可以在 extends 条件语句中使用 infer 关键字来声明一个待推断的类型变量。

通过 ReturnType 理解 infer

infer 相对比较难理解,我们先看下 TypeScript 一个内置工具类型 ReturnType

ReturnType<T> – 获取函数返回值类型。

1
2
const add = (x:number, y:number) => x + y
type t = ReturnType<typeof add> // type t = number

代码解释:

通过 ReturnType 可以得到函数 add() 的返回值类型为 number 类型。但要注意不要滥用这个工具类型,应尽量多的手动标注函数返回值类型。

来看一下 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

infer 的作用是让 TypeScript 自己推断,并将推断的结果存储到一个类型变量中,infer 只能用于 extends 语句中。

再来看 ReturnType 的实现:如果 T 满足约束条件 (...args: any) => any,并且能够赋值给 (...args: any) => infer R,则返回类型为 R,否则为 any 类型。

继续看几个例子:

1
2
3
type T0 = ReturnType<() => string>        // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T> // unknown

代码解释:

分别可以得到 type T0 = string type T1 = void type T2 = unknown,只要满足约束条件 (...args: any) => any,TypeScript 推断出函数的返回值,并借助 infer 关键字将其储存在类型变量 R 中,那么最终得到返回类型 R

借助 infer 实现元组转联合类型

借助 infer 可以实现元组转联合类型,如:[string, number] -> string | number

1
2
3
4
type Flatten<T> = T extends Array<infer U> ? U : never

type T0 = [string, number]
type T1 = Flatten<T0> // string | number

代码解释:

第 1 行,如果泛型参数 T 满足约束条件 Array<infer U>,那么就返回这个类型变量 U

第 3 行,元组类型在一定条件下,是可以赋值给数组类型,满足条件:

1
2
3
4
type TypeTuple = [string, number] 
type TypeArray = Array<string | number>

type B0 = TypeTuple extends TypeArray ? true : false // true

第 4 行,就可以得到 type T1 = string | number

其他例子

1
2
3
4
5
6
7
// 同上,但当a、b为不同类型的时候,返回不同类型的联合类型
type Obj<T> = T extends {a: infer VType, b: infer VType} ? VType : number;

let obj1: Obj<string>; // => number
let obj2: Obj<true>; // => number
let obj3: Obj<{a: number, b: number}>; // => number
let obj4: Obj<{a: number, b: () => void}>; // => number | () => void

infer 理解起来比较抽象,一定要亲手写一下本节中的例子。借助条件类型的 infer 关键字来推断类型,可以实现一些比如联合类型转交叉类型、联合类型转元组的操作,有兴趣的可以了解一下。

工具泛型

TypesScript 中内置了很多工具泛型,前面介绍过 ReadonlyExtract 这两种,内置的泛型在 TypeScript 内置的 lib.es5.d.ts 中都有定义,所以不需要任何依赖都是可以直接使用的。下面看看一些经常使用的工具泛型吧。

Partial

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P]
}

Partial 用于将一个接口的所有属性设置为可选状态,首先通过 keyof T ,取出类型变量 T 的所有属性,然后通过 in 进行遍历,最后在属性后加上一个 ?

我们通过 TypeScript 写 React 的组件的时候,如果组件的属性都有默认值的存在,我们就可以通过 Partial 将属性值都变成可选值。

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
// 该类型已内置在TypeScript中
type Partial<T> = {
[P in keyof T]?: T[P]
};

interface Rectangle {
x: number;
y: number;
width: number;
height: number;
}

type PartialRectangle = Partial<Rectangle>;
// 等价于
type PartialRectangle = {
x?: number;
y?: number;
width?: number;
height?: number;
}

let rect: PartialRectangle = {
width: 100,
height: 200
};

Required

1
2
3
type Record<K extends keyof any, T> = {
[P in K]: T
}

Required 的作用刚好与 Partial 相反,就是将接口中所有可选的属性改为必须的,区别就是把 Partial 里面的 ? 替换成了 -?

Record

1
2
3
type Record<K extends keyof any, T> = {
[P in K]: T
}

Record 接受两个类型变量, Record 生成的类型具有类型 K 中存在的属性,值为类型 T。这里有一个比较疑惑的点就是给类型 K 加一个类型约束, extends keyof any ,我们可以先看看 keyof any 是个什么东西。

1
type ObjKey = keyof any (type ObjKey = String | number | symbol)

大致一直就是类型 K 被约束在 string | number | symbol 中,刚好就是对象的索引的类型,也就是类型 K 只能指定为这几种类型。

我们在业务代码中经常会构造某个对象的数组,但是数组不方便索引,所以我们有时候会把对象的某个字段拿出来作为索引,然后构造一个新的对象。假设有个商品列表的数组,要在商品列表中找到商品名为 「每日坚果」的商品,我们一般通过遍历数组的方式来查找,比较繁琐,为了方便,我们就会把这个数组改写成对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Goods {
id: string
name: string
price: string
image: string
}

const goodsMap: Record<string, Goods> = {}
const goodsList: Goods[] = await fetch('server.com/goods/list')

goodsList.forEach(goods => {
goodsMap[goods.name] = goods
})

other

该object类型旨在抽象出对象的任何键,而Record<K, T>存在以专门定义类型的键。这意味着尝试访问对象属性时存在差异。
TypeScript将允许访问类型对象的任何属性,Record<any, any>即使特定键未知,因为第一个通用参数是any。

1
2
let a: Record<any, any>;
a.foo; // works

object然而,在类型的对象上,不假设键any。与此一样Record<any, …>,TypeScript不知道哪些键实际存在,但它不允许访问任何键:

1
2
3
let b: object;
a.foo; // error: Property "foo" does not exist on type "object"
12

实例:

为防止提示res 没有这个属性:

1
2
let res: Record<string, any>;
res.data && JSON.parse(res.data)

Pick

1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}

Pick 主要用于提取接口的某几个属性。做过 Todo 工具的同学都知道,Todo工具只有编辑的时候才会填写描述信息,预览的时候只有标题和完成状态,所以我们可以通过 Pick 工具,提取 Todo 接口的两个属性,生成一个新的类型 TodoPreview。

1
2
3
4
5
6
7
8
9
10
11
12
interface Todo {
title: string
completed: boolean
description: string
}

type TodoPreview = Pick<Todo, "title" | "completed">

const todo: TodoPreview = {
title: 'Clean room',
completed: false
}

Exclude

1
type Exclude<T, U> = T extends U ? never : T

Exclude 的作用与之前介绍过的 Extract 刚好相反,如果 T 中的类型在 U 不存在,则返回,否则抛弃。现在我们拿之前的两个类举例,看看 Exclude 的返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Worker {
name: string
age: number
email: string
salary: number
}

interface Student {
name: string
age: number
email: string
grade: number
}


type ExcludeKeys = Exclude<keyof Worker, keyof Student>
// 'name' | 'age' | 'email'

type ExcludeKeys = Exclude<keyof Worker, 'name' | 'age'>
// 'name' | 'age'

取出的是 Worker 在 Student 中不存在的 salary

Omit

1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

Omit 的作用刚好和 Pick 相反,先通过 Exclude<keyof T, K> 先取出类型 T 中存在,但是 K 不存在的属性,然后再由这些属性构造一个新的类型。还是通过前面的 Todo 案例来说,TodoPreview 类型只需要排除接口的 description 属性即可,写法上比之前 Pick 精简了一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Todo {
title: string
completed: boolean
description: string
}

type TodoPreview = Omit<Todo, "description">

const todo: TodoPreview = {
title: 'Clean room',
completed: false
}


type TodoPreview2 = Omit<Todo, "description" | "completed">

const todo: TodoPreview2 = {
title: 'Clean room'
}


never

永不存在

never表示的是那些永不存在的值的类型,比如在函数中抛出异常或者无限循环,never类型可以是任何类型的子类型,也可以赋值给任何类型,但是相反却没有一个类型可以作为never类型的子类型,示例如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 函数抛出异常
function throwError(message: string): never {
throw new Error(message);
}

// 函数自动推断出返回值为never类型
function reportError(message: string) {
return throwError(message);
}

// 无限循环
function loop(): never {
while(true) {
console.log(1);
}
}

// never类型可以是任何类型的子类型
let n: never;
let a: string = n;
let b: number = n;
let c: boolean = n;
let d: null = n;
let e: undefined = n;
let f: any = n;

// 任何类型都不能赋值给never类型
let a: string = '123';
let b: number = 0;
let c: boolean = true;
let d: null = null;
let e: undefined = undefined;
let f: any = [];

let n: never = a;
// -> Type 'string' is not assignable to type 'never'.

let n: never = b;
// -> Type 'number' is not assignable to type 'never'.

let n: never = c;
// -> Type 'true' is not assignable to type 'never'.

let n: never = d;
// -> Type 'null' is not assignable to type 'never'.

let n: never = e;
// -> Type 'undefined' is not assignable to type 'never'.

let n: never = f;
// -> Type 'any' is not assignable to type 'never'.