likes
comments
collection
share

ts 类型体操之内置工具类型(上)在 TypeScript 中,utility types是一组预定义的类型,用于在类型

作者站长头像
站长
· 阅读数 22

在 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

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'> 为例:实际执行时

  1. 等于 ('a' extends 'a' | 'c' ? never : 'a') | ('b' extends 'a' | 'c' ? never: 'b')
  2. 等于 (never) | ('b')
  3. 等于 '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>,这里有两个考点:

  1. K 的取值:K 应该是 T 里已经存在的键值,比如你传个 hello 需要抛错
  2. K 是个联合类型,所以需要遍历

我们看看实现:

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

答案还是一个简单的类型映射:

  1. 通过 K extends keyof T 限定 K 必须是T的所有键值的子集
  2. 用个 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)就是了。实现上正好活用一下上面刚提到的方法类型——PickExclude

  1. 从T的所有键中剔除(Exclude)掉联合类型K(Exclude<keyof T, K>
  2. 提取(Pick)出所有键值在上一步得到的结果中的属性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

小结

由于篇幅所限,我们暂时先介绍8个最简单,但又是最贴近实战的工具方法。当你开始使用这些工具类型时,你的新手村小伙伴们一定会眼前一亮的。之后的文章,我会进一步介绍剩下的内置工具类型,当然它们更加复杂也更能帮助我们提升认知。敬请期待。

References

文章同步发布于an-Onion 的 Github。码字不易,欢迎点赞。

转载自:https://juejin.cn/post/7401112990442504226
评论
请登录