[Day14] TS:什麽!TypeScript 中还有回圈的概念 - 用 Mapped Type 操作物件型别

Mapped Type)

上面这个是今天会提到的内容,如果你已经可以轻松看懂,欢迎直接左转去看我队友们的精彩文章!

Index Signatures & Indexable Types

在了解 Mapped Type 之前,需要先来看一下它的前身 Index Signatures。一般来说,在 TypeScript 里定义物件的型别会需要把物件的每一个 key 和 value 的型别都定义清楚,像是这样:

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

但有些时候,因为一些原因,也许是 key 的名称不是那麽重要时,或者 key 的可能太多时,我们可以使用 index signatures 来定义这个物件,例如,定义一个 key 为 string,value 则为 stringnumber 的型别:

type PersonDict = {
  // "key" 可以是取成任何名称
  [key: string]: string | number;
};

[key: string] 中的这个 key 可以是任何名称,你也可以改成 [property: string] 效果是一样的。

Mapped Type 和 in operator

在了解 index signatures type 後,就可以来初步认识 Mapped Type。而在 Mapped Type 中最重要的就是 in 这个关键字的使用。

先来看看 in 怎麽用:

type PersonMap = {
  [key in 'firstName' | 'lastName']: string;
};

这里和 index signatures 类似,你一样会看到像是 [key: ... ] 这样的写法,key 一样是可以自己取的变数名称,而不一样的是多了 in 这个关键字。

这个 in 的感觉非常「类似」在 JavaScript 中 Array 的 for...of 方法,上面的 [key in 'firstName' | 'lastName'] 可以想成是这样的感觉:

// Mapped Type 中的 [key in 'firstName' | 'lastName'],很类似於 for...of 的方法
for (const key of ['firstName', 'lastName']) {
  console.log(key);
}

for (const key of ...) 中,这个 key 会是每次叠代时阵列取得的元素值,所以这里的话第一次会是 firstName,第二次会是 lastName

回到 Mapped Type 中的 [key in 'firstName' | 'lastName'],这里的 key 也是很类似的概念,可以想像成会跑一个回圈,第一次 key 的值会是 firstName,第二次的值则是 lastName

也就是说,上面的:

type PersonMap = {
  [key in 'firstName' | 'lastName']: string;
};

其实就是:

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

:::tip

把 Mapped Type 中的 key in ... 想成是类似 for (const key of ...) 的概念,是我认为理解 mapped type 最重要的一步。它都有跑一个回圈把所有元素依序取出来的概念。

:::

由於 key 只是一个变数名称,它也可以命名成其他名称,又因为它表示的是物件属性(property),所以也很常会用 P 来表示它,例如下面建立另一个 mapped type:

type Device = {
  [P in 'apple' | 'samsung' | 'google']: string;
};

如果了解刚刚的说明的话,应该可以想到,这里 P 就会依序是 applesamsungpixel,出来的型别会等同於:

type Device = {
  apple: string;
  samsung: string;
  pixel: string;
};

这里你也可以理解到 mapped type 和 indexed signature 的差别,mapped type 可以视为是 indexed signatures 的子集合(subset),它能将物件的属性定义的更明确,而不是单纯用某个型别来表示:

// index signatures:物件的属性只要是 string 即可
type DeviceDict = {
  [key: string]: string;
};

// mapped type:物件的属性需要是 'apple' | 'samsung' | 'google'
type DeviceMap = {
  [P in 'apple' | 'samsung' | 'google']: string;
};

单纯看到这里,可能还是感受不到为什麽会说 Mapped Type 是可以用来操作型别的「型别魔术师」,只会觉得 Mapped Type 单纯只是可以「跑回圈」用的。

搭配 keyof 修改 Object Type 中所有 value 的型别

让我们回到今天的例子。

假设现在我们定义了一系列的事件:

type SupportedEvent = {
  click: string;
  change: string;
  keyup: string;
  keydown: string;
};

这时候如果想根据 SupportedEvent 中属性的名称,产生一个新的型别叫做 HandledEvent,但物件 value 的型别要全部换成 function 的话,我们当然可以如同过去一个一个把属性定义出来:

type HandledEvent = {
  click: () => void;
  change: () => void;
  keyup: () => void;
  keydown: () => void;
};

但这麽做除了很麻烦之外,未来如果有新增支援的事件类型到 SupportedEvent 的话,还需要同时记得加到 HandledEvent 这个型别中,如果忘记加的话,两个型别中支援的 Event 类型就会不一致。

这时候使用 Mapped Type 搭配 keyof 就会非常的方便:

type HandledEvent = {
  [K in keyof SupportedEvent]: () => void;
};

写成这样就搞定了,未来如果 SupportedEvent 中有新增的事件类型是,HandledEvent 也不需要额外改动,如此将符合 Single Source of Truth 的概念。

现在来了解一下我们刚刚的组合技是怎麽使用的。

首先要注意到的是 [K in keyof SupportedEvent]

  • 前面有提到 K 只是一个变数名称,会对应到的是 in 後面每次取出来的值。

接着把注意力放到 in 後面的内容,它是 keyof SupportedEvent

  • 搭配前面对於 keyof 的理解,可以知道 keyof SupportedEvent,对应到也就会是 click | change | keyup | keydown

也就是说如果换成 JavaScript for ... of 的方法,它会像是这样:

//  [K in keyof SupportedEvent]
for (const K of ['click', 'change', 'keyup', 'keydown']) {
  /* ... */
}

这样你就可以知道,K 其实对应到的就会是 clickchangekeyupkeydown

再来是 [K in keyof SupportedEvent]: () => void;: 後的内容就会是物件 value 的型别。因为我们想要把它改成 function,所以就在後面放了 () => void

Mapped Type 学习重点

  • 把 Mapped Type 中的 [KEY in ...] 想成是类似 for (const key of ...) 的概念,是我认为理解 mapped type 最重要的一步。它们都有跑一个回圈把所有元素依序取出来的概念。
  • 操作物件型别时,要很快联想到 Mapped Types,不论是想改变物件属性 key 的名称或 value 的型别,都可以透过。Mapped Types 达到。

范例程序码

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

参考资料


<<:  29.移转 Aras PLM大小事-额外编码取号(3)

>>:  DAY 14 - 哥布林 (1)

[Day04] Wordpress

Wordpress 的特色 根据 Wordpress 的官方网站,全球有超过 42% 的网站使用 W...

VPN和EAP

-VPN 和 EAP 在 802.1X 中,请求者与身份验证者通信,身份验证者将身份验证消息转发到...

Day 13 - AI-900 认证心得(1) - 准备

上一篇谈了疫情期间Azure 提供了四种基础证照的免费考试, 因此我也在五~六月的三级防疫期间在M...

[重构倒数第09天] - Vue-Cli + PurgeCSS 删除你用不到的CSS

前言 该系列是为了让看过Vue官方文件或学过Vue但是却不知道怎麽下手去重构现在有的网站而去规画的系...

2 游戏规则

所以到底是要做怎样 昨天列出了一些还在考虑的点,做了一些粗暴的决定: Q: 要让这个游戏可以用桌游的...