[Day17] TS:理解 Pick、Record 的实作

Record

这是我们今天要聊的内容,老样的,如果你已经可以轻松看懂,欢迎直接左转去看同事 Andy 精彩的文章 — 「前端工程师学习 DevOps 之路」。

在学习了 Mapped Types 後,我们已经把多数要用来操作型别所需的知识补齐了,後面几天就来看一些实际会用到的实作,包括 TypeScript 中内建以及其他第三方提供的 Utility Types。

今天让我们先来看 PickRecord

Pick

在 TypeScript 内建的 Utility Types 中,有一个 Pick<T, K extends keyof T> ,使用方式很简单,它可以帮选择要保留下物件型别中的那些属性,例如:

type Person = {
  firstName: string;
  lastName: string;
  age: number;
};

type PersonName = Pick<Person, 'firstName' | 'lastName'>;

PersonName 的型别就会等同於这样:

type PersonName = {
  firstName: string;
  lastName: string;
};

在有了前面关於型别操作的知识後,会发现 Pick 的原始码其实也蛮单纯好理解的:

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

我们可以看到 Pick 吃两个型别参数,一个是 T 它会是物件型别,後面的 K 因为有用的 Day03 提的泛型限制,所以 K 一点要满足是物件型别 T 中有的属性 key。

接着 [P in K] 很明显的是 Mapped Types,以上面提的 PersonName 为例,K 就会是 'firstName' | 'lastName',这时候的 [P in 'firstName' | 'lastName'] 读者们应该可以预想到最後出来的型别其物件型别的属性 key 会长这样:

{
  firstName: '...';
  lastName: '...';
}

最後看到属性 value 的部分是 T[P],意思也就是,什麽都不做,原本物件属性值的型别是什麽就直接拿来用,因此最後 PersonName 的型别会长这样:

type PersonName = {
  firstName: string;
  lastName: string;
};

Record

接着我们来看一下 Record<Keys, Type>。前面我们有提过 Mapped Types 是比较有限制的 Index Signatures,也就是 Mapped Types 是 Index Signatures 的子集合,而 Record 这个 Utility Type 同样是基於 Mapped Types 写出来的,先来简单看一下它的用法。

在没有 Record 之前

假设现在需要建立一个物件型别,它的 key 希望符合 ConferenceName、value 符合型别 Conference

type ConferenceName = 'ModernWeb' | 'MOPCON' | 'JSDOC' | '{Laravel x Vue}';
type Conference = {
  name: string;
  year: number;
  isAddToCalendar: boolean;
  website: string;
};

前面 Day14 曾提过,因为 Index Signatures 的特性,并没有办法直接写:

type ConferenceIndexSignatures = {
  [K: ConferenceName]: Conference;
};

TypeScript 会报错,并建议我们用 Mapped Type:

Record

於是用 Mapped Types 可以写成这样:

type ConferenceMap = {
  [P in ConferenceName]: Conference;
};

接着我们把它改成更泛用的形式试试看,先把 ConferenceName 抽成泛型:

// 把 ConferenceName 变成泛型
type ToConferenceMap<K> = {
  [P in K]: Conference;
};

这时候你会看到 TypeScript 报错:

Record

之所以会有这个错误的原因是 K 没有办法被保证一定能把叠代,如果 K 被带入物件型别的话,[P in K] 就会坏掉,因此 TypeScript 说 K 应该只能是 string | number | symbol,而这其实也就是 keyof any 的意思,因此可以透过泛型限制来限制使用者可以带入的 K

type ToConferenceMap<K extends keyof any> = {
  [P in K]: Conference;
};

接着我们来把物件型别的属性值也抽成泛型:

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

有没有发现刚刚写的 ToConferenceMap 变的更泛用了!现在我们可以使用刚刚自己写的这个 Utility Type 来产生 ConferenceMap

type ConferenceName = 'ModernWeb' | 'MOPCON' | 'JSDC' | '{Laravel x Vue}';
type Conference = {
  name: string;
  year: number;
  isAddToCalendar: boolean;
  website: string;
};
type ToConferenceMap<K extends keyof any, T> = {
  [P in K]: T;
};

type ConferenceMap = ToConferenceMap<ConferenceName, Conference>;

如此就可以得到我们想要的物件型别:

Record

更重要的是,其实我们已经写出了官方提供的 Record 了!

使用 Record 与原始码

在 TypeScript 中,Record<Keys, Type> 可以让开发者方便定义物件型别中属性 key 和 value 的型别,而你会发现用 Record 建立出来的型别,和用刚刚我们写的 ToConferenceMap 的效果一样:

// 两个建立出来的型别是一样的
type ConferenceMap = ToConferenceMap<ConferenceName, Conference>;
type ConferenceRecord = Record<ConferenceName, Conference>;

回过头来看 Record 的原始码:

// Construct a type with a set of properties K of type T
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

有没有发现和我们刚刚写的 ToConferenceMap 其实是一样的呢?

建立自己的 Utility Types

读者们应该慢慢可以发现,要写出 Utility Types 比想像中的简单,可以先根据实际的情况撰写出想要的结果後,再把可以抽成参数的部分变成泛型,最後就可以建立出更泛用的 Utility Types。

当然也是有一些很复杂的 Utility Types 可能没办法这麽简单就写的出来,但重要的是能够透过正确的断句,先求能够看懂和理解。

范例程序码

https://tsplay.dev/WJ8E5N @ TypeScript Playground

参考资料


<<:  有时差的我如何跟团队协作

>>:  Day 17 - 取得帐务相关资讯 (下)

Day12 -画布操作与编织复杂图形2

Scale() - 缩放 相对於目前的画布大小进行缩放,如 scale(0.5)。 -> 变成...

Day 27 KubeEdge小专题: Grafana部署

今天要说明的是Grafana部署的部分。依照在Day 23 中的软件架构图在云端与边缘端各自布署了一...

Day11 数据图表化 - 图表功能介绍

Kibana是一套分析和视觉化的软件,可以快速的帮助使用者更好的应用和分析资料。在接下来我们要开始介...

30天学习笔记 -day 26-Motion Editor(上篇)

Motion Editor是自 Android Studio 4.0 版本开始为MotionLayo...

Rust-流程控制-if

利用布林值来决定如何继续执行程序进行决策 例 let n = 3; if n > 2 { pr...