ts 类型体操之内置工具类型(上)

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

  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个最简单,但又是最贴近实战的工具方法。当你开始使用这些工具类型时,你的新手村小伙伴们一定会眼前一亮的。之后的文章,我会进一步介绍剩下的内置工具类型,当然他们更加复杂也更能帮助我们提升认知。敬请期待。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容