在 TypeScript 中,内置工具类型(utility types)是一组预定义的类型,用于在类型层面上进行各种操作。对于 ts 开发者来说,开始使用这类工具是一个走出新手村的重要标志。截止至 2024 年 8 月,ts 官方共提供了 22 个内置的工具类型。大家可以在官网查看具体的文档。当然,本文并不是来集中介绍这些类型的用法,我们要更近一步,来看看如何用更底层的类型方法来实现这些工具类型。
Record
我们先从最简单的入手
Record<K, T>
将 K
中的每个属性值转化为 T
类型,例如:
type Animal = 'Dog' | 'Cat';
type AnimalRecord = Record<Animal, string>;
// type AnimalRecord = {
// Dog: string;
// Cat: string;
// }
Record 的实现如下:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type K = keyof any; // string | number | symbol
Record 是最最常用的一个工具类型,实现也极其简单,只需要用到我们在上期中介绍的类型映射。简单遍历第一个泛型 K 的每一个属性,并将属性值都转成第二个泛型 T 类型。 这里对 K 做了限制,就是它只能是 string、 number 和 symbol 的一种。我们再简单展开一下, keyof any
等价于联合类型string | number | symbol
;如果是老手,你可能还会知道 ts 定义了一个原生类型 type PropertyKey = string | number | symbol
,有时候偷个懒不想写一大串string | number | symbol
,可以直接使用PropertyKey
秀一把。
Partial & Required & Readonly
Partial<T>
: 将 T
的所有属性变为可选,例如:
type Vegetable = {
Onion: string;
Garlic: number;
};
type PartialVegetable = Partial<Vegetable>;
// type PartialVegetable = {
// Onion?: string;
// Garlic?: number;
// }
Partial 的实现如下:
type Partial<T> = {
[P in keyof T]?: T[P];
};
这里有个知识点: 在冒号前加个 ?
(等价于+?
)就表示该键值的类型是可选类型(即有可能是 undefined).
Required<T>
: 把所有属性变成必选
有 +?
操作,自然也有 -?
,Required 就是Partial的反向操作:
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Vegetable = {
Onion?: string;
Garlic?: number;
};
type RequiredVegetable = Required<Vegetable>;
// type RequiredVegetable = {
// Onion: string;
// Garlic: number;
// }
Readonly<T>
: 将所有属性变成只读
类似加减 ?
的操作还有一个就是:加减 readonly
,只不过 readonly
要放在属性的最前面。
再看看 Readonly
的实现(这里readonly
等价于+readonly
):
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
正好,我们再做个练习题: Mutable
实现通用的
Mutable<T>
, 使得T
中的所有属性都是可变的(不是只读的)
interface Todo {
readonly title: string;
readonly completed: boolean;
}
type MutableTodo = Mutable<Todo>; // { title: string; completed: boolean; }
很简单,-readonly
就行
type Mutable<T extends object> = {
-readonly [K in keyof T]: T[K];
};
Exclude & Extract & Pick & Omit
我们稍增加一点难度,实现一些有两个泛型的类型
Exclude<T, U>
: 从T
中剔除那些可赋值给U
的类型
Exclude主要用户联合类型的造作。如下所示从联合类型 a' | 'b'
中剔除c
( c
是 'a' | 'c'
的子集 ) 得到 b
type C = Exclude<'a' | 'b', 'a' | 'c'>; // 'b'
答案很简单直接用 extends 判断就行了:
type Exclude<T, U> = T extends U ? never : T;
不过这里要补充个 extends 的知识点,
T extends U ? never : T
实际执行时是对联合类型T
里的每一个元素分别进行条件判断,然后对每一个条件判断的结果再组装成新的联合类型。以 Exclude<'a' | 'b', 'a' | 'c'>
为例:实际执行时
- 等于
('a' extends 'a' | 'c' ? never : 'a') | ('b' extends 'a' | 'c' ? never: 'b')
; - 等于
(never) | ('b')
; - 等于
'b'
(任何元素和never
的联合类型等于其本身)
联合类型的条件判断本质上在进行“遍历”,这是个很有趣的语法特性。我们这里暂不展开了,之后我会在实际的案例中解释如何用这个特性解决一些需要依靠遍历来破解的问题。
Extract<T, U>
: 从T
中提取可赋值给U
的类型
Exclude的反向操作就是Extract,就是剔除不包含在U里的类型。这个太简单了,一笔带过:
type Extract<T, U> = T extends U ? T : never;
Pick<T, K>
: 从 T
中,提取出所有键值在联合类型 K
中的属性
如下所示,我只想保留 Todo 类型里的 title 和 completed键值对:
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, 'title' | 'completed'>;
// type TodoPreview = {
// title: string,
// completed: boolean
// }
对Pick<T, K>
,这里有两个考点:
-
K
的取值:K
应该是T
里已经存在的键值,比如你传个hello
需要抛错 -
K
是个联合类型,所以需要遍历
我们看看实现:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
答案还是一个简单的类型映射:
- 通过
K extends keyof T
限定 K 必须是T的所有键值的子集 - 用个
in
遍历K
就行了 (in keyof
不是固定组合……)
Omit<T, K>
: 构造一个除类型K
以外具有T
属性的类型。
Omit是Pick的反向操作,排除对象 T
中的 K
键值。 Omit在名字上容易和Exclude搞混。记住 Exclude 主要用在联合类型,而Omit主要用于对象类型上。如下所示,我要剔除Todo里的description和title两个键值对:
type TodoPreview = Omit<Todo, 'description' | 'title'>;
// type TodoPreview = {
// completed: false,
// }
Omit<T, K>
对K没有特别限制,只需要是正常的JS对象键类型(string | number | symbol
)就是了。实现上正好活用一下上面刚提到的方法类型——Pick
和Exclude
:
- 从T的所有键中剔除(
Exclude
)掉联合类型K(Exclude<keyof T, K>
) - 提取(
Pick
)出所有键值在上一步得到的结果中的属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
小结
由于篇幅所限,我们暂时先介绍8个最简单,但又是最贴近实战的工具方法。当你开始使用这些工具类型时,你的新手村小伙伴们一定会眼前一亮的。之后的文章,我会进一步介绍剩下的内置工具类型,当然他们更加复杂也更能帮助我们提升认知。敬请期待。
文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞。