跳至主要内容

[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 Parameter 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

參考資料