[Day19] TS:什麽!泛型的参数还能有预设值?

Generic parameter defaults

今天这个范例是来自第三方套件 utility-types,在有了前几天的知识後,让我们来试着了解这个 Utility Type 是如何实作的吧!如果你已经可以轻松看懂,欢迎直接左转去看我队友们的精彩文章!

Optional 的用法

要把物件型别中的属性全部变成 Optional 的话,在 Day16 时曾经提过可以使用官方内建的 Partial,但假设今天只想要让这个物件型别中的部分属性变成 Optional 的话,就可以用这里的这个 Optional<T, K>

备注:这里的 Optional 并不是 TypeScript 中内建的 Utility Type,读者如果要使用的话,记得要先复制这个 Utility Type 的原始码到程序码中。

举例来说,现在有一个型别 Conference

type Conference = {
  name: string;
  year: number;
  isAddToCalendar: boolean;
  website: string;
};

後来发现 Conference 这个型别中,yearisAddToCalendar 都可以省略(Optional),只有名称和网址是必填的,这时候就可以用 Optional 来达到:

type ConferenceWithOptional = Optional<Conference, 'year' | 'isAddToCalendar'>;

这时候这个 ConferenceWithOptional 就会变成是:

// year 和 isAddToCalendar 变成 optional 的
type ConferenceWithOptional = {
  name: string;
  year?: number;
  isAddToCalendar?: boolean;
  website: string;
};

但这个 Optional 还有一个蛮特别的地方,它也可以只带入物件型别给它就好,而不告诉它那些属性 Key 是要变成 optional 的:

// 没有带入 Optional 的第二个参数
type ConferenceWithAllOptional = Optional<Conference>;

这时候 ConferenceWithAllOptional 它「预设」就会把该物件型别中的所有 Key 都变成 Optional 的了:

// 预设会把所有属性都变 Optional
type ConferenceWithAllOptional = {
  name?: string;
  year?: number;
  isAddToCalendar?: boolean;
  website?: string;
};

这里我们发现两个重要的点:

  1. 过去的 Utility Type 只要定义了几个泛型参数,开发者就需要带入几个参数进去,不能多也不能少,但这里却可以带一个参数或带入两个参数都可以。
  2. 多了「预设值」的概念,如果没给所有参数,预设会把所有 Key 都变成 optional。

很特别吧!让我们来理解看看它是怎麽被实作的吧!

理解 Optional 的实作

Optional 这个 Utility Type 的原始码如下:

type Optional<T extends object, K extends keyof T = keyof T> = Omit<T, K> &
  Partial<Pick<T, K>>;

要了解原始码,最重要的就是要知道如何做出正确的断句,让我们先把注意力放到 = 的前面:

Optional Utility Type

没错,这一长串都是 = 前面,从 <T extends ...> 开始,可以理解到 Optional 它吃两个参数 TK

Optional Utility Type

Day03 中我们提过这是属於泛型限制的写法,所以 T extends object 就是 T 需要是 object 的子集合;後面的 K extends keyof T = keyof T 好像出现了我们不曾看过的语法,没错!这就是今天的重点「泛型参数预设值(Generic parameter defaults)」。

泛型参数预设值(Generic Paramter Defaults)

泛型参数预设值的用法就和 JavaScript 函式中带入参数预设值的方式一样,都是用等号(=),在知道泛型的参数也能带入预设值之後,回过头来看刚刚的 K extends keyof T = keyof T

  1. K extends keyof T 的意思是: K 需要满足 keyof T,也就是说,K 需要是 T 这个物件型别中所包含的属性 key。
  2. K ... = keyof T 的意思就是,如果没给 K 的话,预设就让 K 的型别等同於 keyof T,也就是预设的 K 会是所有物件型别中的所有 key。

备注:泛型参数预设值并没有一定要搭配泛型限制(extends)使用。

带入实际的型别帮助理解

接着把重点放到 = 的後面:

Optional Utility Type

虽然前几天曾提过 OmitPartialPick 的用法,但可能还是会忘,这时候把握前面提过的原则:「不确定时就带入实际的型别试试看」:

type A = Omit<Conference, 'year' | 'isAddToCalendar'>;
type B = Pick<Conference, 'year' | 'isAddToCalendar'>;
type C = Partial<Pick<Conference, 'year' | 'isAddToCalendar'>>;

你会发现 A 其实就是把要变成 Optional 的属性「忽略」掉,只留下不改变的部分:

Optional Utility Type

B 就是把要变成 Optional 的属性「挑出」来:

Optional Utility Type

最後的 C 只是把 B 当成参数带入 Partial 中,让这些物件属性全都变 optional 的。所以最後的 Result 就会是「没被挑到的什麽都不做(A)」加上「被挑到的都变成 Optional(C)」:

type A = {
  name: string;
  website: string;
};
type C = {
  year?: number | undefined;
  isAddToCalendar?: boolean | undefined;
};

type Result = A & C;

Result 就会是:

type Result = {
  name: string;
  website: string;
  year?: number | undefined;
  isAddToCalendar?: boolean | undefined;
};

透过带入实际型别的方式,回过头来看 Optional 这个 Utility Type 的回传值 Omit<T, K> & Partial<Pick<T, K>> 就更能够理解它的意思。

为什麽这里要使用泛型参数预设值?

最後,你可能会好奇,为什麽这里需要帮型别加上预设值呢?「当你不清楚为什麽要多这个的时候,最好的方式就是把它拿掉试试看会发生什麽事」,因此如果我们把 Optional 原始码中的泛型参数预设值拿掉(即,删掉= keyof T 的部分),改成:

Optional Utility Type

结果会发现,原本我们只带入一个参数用法的地方跳出错误了,因为和 JavaScript 的函式一样,在没给参数预设值的情况下,每个参数都需要带好带满才行:

Optional Utility Type

范例程序码

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

参考资料


<<:  D19 - 彭彭的课程# Python 文字档案的读取和储存(1)

>>:  IT 铁人赛 k8s 入门30天 -- day20 k8s Logging Architecture

Day16 - 【概念篇】OAuth flows: Refresh Token

本系列文之後也会置於个人网站 +--------+ +---------------+ | |--...

Day5 Android - Layout版面(下)

继昨天讲了的ConstraintLayout,今天要来介绍自己也常用的另外两个布局,LinearLa...

使用证书对代码进行签章,以防止其被篡改并向用户验证您的身份-使用您的私钥对代码进行散列并加密结果

-使用私钥和公钥对强大的程序集进行签名和验证 (来源:https://flylib.com/boo...

[Day18] 刺激! 居家上班之老板v.s.员工偷懒攻防战!

这边要强调一下,我相当珍惜公司给予的居家上班机会, 完全没有想过或做过以下行为,只是以少数人情况举例...

[GMI/GMA] 透过移动装置连上 Genero Web App

至目前的章节为止,已经可以执行 Genero FGL的程序在 Windows/MAC/Linux ...