Typescript中的工具类型

前言

写写那些Typescript自带的工具类型吧,可能原理方面偏少,更多的是翻译

正文

Typescript - Utility Types

Typescript自带的工具类型,有些在源码中也经常的使用到,比如RecordVue@next中就有用到

为了在IDE中使用Typescript,本文使用全局安装方式(yarn global add typescriptnpmnpm install -g typescript

然后在IDEATypescript配置安装路径

IDEA 2020.2默认有内置的Typescript,版本为3.9.5,不过现在是4.0.5

Partial<T>

Constructs a type with all properties of 'T' set to optional.
This utility will return a type that represents all subsets of a given type.

构造一个传入类型T的所有属性都为可选的类型

简单地讲就是使得泛型T的所有属性变为可选的

1
2
3
4
5
6
7
8
9
10
11
12
13
interface User {
id: number;
name: string;
}

// id和name都变成可选的了
type UserPartial = Partial<User>;

function fn(userPartial: UserPartial) {}

fn({}); // 可以
fn({ id: 1 }); // 可以
fn({ name: "lwf" }); // 可以

可以看下它的源码

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

使用keyof来获取全部的属性名,然后使用in来遍历,使用?使得属性变为可配置,右侧的值为T[P]对应每个属性对应的类型

Readonly<T>

Constructs a type with all properties of 'T' set to readonly,
meaning the properties of the constructed type cannot be reassigned.

构造一个传入类型T的所有属性都为只读的类型,这意味着构造出来的类型的属性无法被重新赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface User {
id: number;
name: string;
}

type UserReadOnly = Readonly<User>

const user: UserReadOnly = {
id: 1,
name: "lwf"
};

user.id = 1; // 报错
user.name = "fwl" // 报错

注意这里的只读只对这个对象的属性生效,嵌套的属性不生效

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
interface User {
id: number;
name: string;
info: {
age: number;
sex: "male" | "female";
}
}

type UserReadOnly = Readonly<User>

const user: UserReadOnly = {
id: 1,
name: "lwf",
info: {
age: 22,
sex: "male"
}
};

user.info = {
age: 11,
sex: "female"
}; // 报错

user.info.age = 11; // 可以

源码如下

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

依然使用keyof来获取全部的属性名,然后使用in来遍历,右侧值为对应的类型

然后对每个属性前都用readonly来标注,使之成为只读的

如果想让一个数组变为只读,可以使用Readonly<Array<T>>,比如

1
2
3
const arr: Readonly<Array<any>> = [];

arr[1] = 1; // 报错

其实Typescript已经内置了只读的数组类型ReadonlyArray<T>

1
2
3
const arr: ReadonlyArray<number> = [];

arr[1] = 1; // 报错

对于ReadonlyArrayArray的区别就是

ReadonlyArray在整数属性和length上使用了readonly来修饰,而Array没有

并且ReadonlyArray少了那些会使得数组发送改变的方法,比如pushpopshiftunshift,如下(省略了注释以及相同的API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Array<T> {
length: number;
pop(): T | undefined;
push(...items: T[]): number;
reverse(): T[];
shift(): T | undefined;
sort(compareFn?: (a: T, b: T) => number): this;
splice(start: number, deleteCount?: number): T[];
splice(start: number, deleteCount: number, ...items: T[]): T[];
unshift(...items: T[]): number;
[n: number]: T;
// 其他API...
}

interface ReadonlyArray<T> {
readonly length: number;
readonly [n: number]: T;
// 其他API...
}

sortreverse由于会交换数组中元素的位置(交换步骤会涉及到赋值),所以没有在ReadonlyArray

Record<K, T>

Constructs a type with a set of properties 'K' of type 'T'.
This utility can be used to map the properties of a type to another type.

构造一个属性为K的集合,属性对应的类型为T的类型
这个工具类型可以把一个类型的属性映射到另一个类型

1
2
3
4
5
6
7
8
9
10
11
let record: Record<string, Function>;

for (let i = 0; i < 3; i++) {
record[i] = function () {
return i + 1;
}; // 可以
}

for (let i = 0; i < 3; i++) {
record[i] = i + 1; // 报错
}

源码如下

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

这里可能会疑惑keyof any是个什么东西,可以试着给某个变量赋予看一下

发现变量o类型为number或者symbol或者number

也就是泛型K必须是这三个类型中的其中一种,如果使用Function作为范型K,是无效的

1
let record: Record<Function, string>;   // 报错

一般都是使用Record<string, T>来生成一个类型为T的映射对象

Pick<T, K>

Constructs a type by picking the set of properties 'K' from 'T'.

构造一个从T中选取K属性集的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
interface User {
id: number;
name: string;
}

// 现在UserPick类型只有id属性了
type UserPick = Pick<User, "id">;

let user: UserPick = {
id: 1
}; // 可以

user.name = "lwf"; // 报错

源码如下

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

keyof T获取T的属性集,依然使用in来遍历属性集,每个属性对应类型为T[P]

这里可以看出K必须是T属性集的一个子集,不是的话会报错

1
2
3
4
5
6
7
interface User {
id: number;
name: string;
}

// age没有在User中,报错
type UserPick = Pick<User, "id" | "age">;

Omit<T, K>

Constructs a type by picking all properties from 'T' and then removing 'K'.

构造一个从T中移除K属性集的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
interface User {
id: number;
name: string;
}

// id被移除了,现在只有name了
type UserOmit = Omit<User, "id">;

let user: UserOmit = {
name: "lwf"
}; // 可以

user.id = 1; // 报错

源码如下

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

可以看到,实现依赖了PickExclude,可以先在左边点击Exclude查看

Exclude<keyof T, K>提取T中不包含K的部分,这时得到的只是属性名的集合类型,而不是属性名对应属性值类型

再使用Pick<T, Exclude<keyof T, K>>把这部分提取出来,这时就变成属性名对应属性值类型

Exclude<T, E>

Constructs a type by excluding from 'T' all union members that are assignable to 'E'.

构造一个从T中排除所有可以分配给E的联合成员的类型

1
2
3
type t1 = Exclude<"a" | "b" | "c", "c">;    // "a" | "b"
type t2 = Exclude<string | number, 1 | "" | false>; // string | number
type t3 = Exclude<1 | "" | false, string | number>; // false

类型t1应该很好理解,有趣的应该是t2t3

t2由于number无法分给1,以及string无法分给空字符串,所以还是string | number

t3t2相反,1可以分给number,以及空字符串可以分给string,所以t3类型为false

Extract<T, U>

Constructs a type by extracting from 'T' all union members that are assignable to 'U'.

构造一个从T中提取所有可以分配给E的联合成员的类型

这个和Exclude<T, E>相反,大白话就是求交集,但是有单向的关系

1
2
3
type t1 = Extract<"a" | "b" | "c", "c">;    // "c" 
type t2 = Extract<string | number, 1 | "" | false>; // never
type t3 = Extract<1 | "" | false, string | number>; // 1 | ""

t1还是很好理解,重点还是t2t3

t2由于string无法分给空字符串,以及number无法分给1,导致"并集"为空,所以为never

t3由于1可以分给number,以及空字符串可以分给string,所以并集就为1 | ""

源码如下

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

NonNullable<T>

Constructs a type by excluding null and undefined from 'T'.

构造一个从T中排除nullundefined的类型

1
type t = NonNullable<number | string | null | undefined>;   // number | string

源码如下

1
type NonNullable<T> = T extends null | undefined ? never : T;

Parameters<T>

Constructs a tuple type from the types used in the parameters of a function type 'T'.

构造一个类型T函数的参数类型的元素类型

通俗点讲就是提取函数的参数,用数组装起来

1
2
3
type f1 = (id: number, name: string) => void;

type args = Parameters<f1>; // [ id: number, name: string ]

源码如下

1
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ConstructorParameters<T>

Constructs a tuple or array type from the types of a constructor function type.
It produces a tuple type with all the parameter types (or the type never if Type is not a function).

构造一个从构造函数的类型的元组或者数组类型,产生了一个所有参数类型的元组类型(或者当T不是一个函数时为never类型)

简单点讲就是提取构造函数的参数类型

1
2
3
4
5
interface User {
new(id: number, name: string): void
}

type UserParameters = ConstructorParameters<User>; // [ id: number, name: string ]

源码如下

1
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

这里和Parameters的区别就是ConstructorParameters必须为构造函数,而Parameters必须为普通函数,两着无法相互转换

ReturnType<T>

Constructs a type consisting of the return type of function 'T'.

构造一个由T函数类型的返回类型组成的类型

简单点讲就是提取函数返回的类型

1
2
3
4
5
6
7
8
9
10
interface User {
id: number;
name: string;
}

interface UserService {
getUser(id: User["id"]): User;
}

type getUserReturnType = ReturnType<UserService["getUser"]>; // User

源码如下

1
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

InstanceType<T>

Constructs a type consisting of the instance type of a constructor function in 'T'.

构造一个T构造函数的实例类型的类型

简单点讲就是返回某个构造函数产生的实例的类型

1
2
3
4
5
interface User {
new(id: number, name: string): string;
}

type a = InstanceType<User> // string

源码如下

1
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

Required<T>

Constructs a type consisting of all properties of 'T' set to required.
The opposite of Partial.

构造一个T类型的所有属性集都是必须的类型
Partial操作的相反操作

1
2
3
4
5
6
7
8
9
10
interface User {
id: number;
name?: string;
}

type UserRequired = Required<User>;

let user: UserRequired = {
id: 1
}; // 报错,提示缺失name

源码如下

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

ThisParameterType<T>

Extracts the type of the this parameter for a function type,
or unknown if the function type has no this parameter.

提取一个函数类型的this参数类型, 如果函数类型没有this参数,那么为unknown

1
2
3
4
5
6
7
8
type Context = {
id: number;
name: string;
}

type f1 = (this: Context) => void;

type t = ThisParameterType<f1>; // Context

源码如下

1
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

OmitThisParameter<T>

Removes the this parameter from T. If T has no explicitly declared this parameter, the result is simply T.
Otherwise, a new function type with no this parameter is created from T.
Generics are erased and only the last overload signature is propagated into the new function type.

删除Tthis参数,如果T没有明确的定义this参数,结果为简单的T
否则,从T生成一个没有this参数的新的函数类型
泛型会被擦除,只有最后的重载签名会被传递到新的函数类型中。

1
2
3
4
5
6
7
8
type Context = {
id: number;
name: string;
}

type f1 = (this: Context, age: number) => void;

type t = OmitThisParameter<f1>; // (age: number) => void

源码如下

1
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;

ThisType<T>

This utility does not return a transformed type. Instead, it serves as a marker for a contextual this type.
Note that the –noImplicitThis flag must be enabled to use this utility.

这个工具不会返回一个转换的类型,而是用于标记一个this的上下文的类型
使用--noImplicitThis标志才能够使用这个工具

这个有点混入的意思在里面,这里借用官方的例子,记得在tsconfig.json中配置

1
2
3
4
5
{
"compilerOptions": {
"noImplicitThis": true
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}

let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx;
this.y += dy;
// 如果没有加ThisType<D & M>,这里点不到x和y
},
},
});

源码如下

1
interface ThisType<T> { }

后记

重装了下npmyarn,明明把安装路径放D盘了,还是装到C盘…

看起来还是非常不错的工具类,不过实现还是有些看不懂,比如泛型的?:加上extends,得找个时间好好看看…

以及一些有趣的语法,比如inferreadonly-?