
今天這個範例是來自第三方套件 utility-types,在有了前幾天的知識後,讓我們來試著了解這個 Utility Type 是如何實作的吧!如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!
要把物件型別中的屬性全部變成 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 這個型別中,year 和 isAddToCalendar 都可以省略(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;
};
這裡我們發現兩個重要的點:
很特別吧!讓我們來理解看看它是怎麼被實作的吧!
Optional 這個 Utility Type 的原始碼如下:
type Optional<T extends object, K extends keyof T = keyof T> = Omit<T, K> &
Partial<Pick<T, K>>;
要了解原始碼,最重要的就是要知道如何做出正確的斷句,讓我們先把注意力放到 = 的前面:

沒錯,這一長串都是 = 前面,從 <T extends ...> 開始,可以理解到 Optional 它吃兩個參數 T 和 K:

在 Day03 中我們提過這是屬於泛型限制的寫法,所以 T extends object 就是 T 需要是 object 的子集合;後面的 K extends keyof T = keyof T 好像出現了我們不曾看過的語法,沒錯!這就是今天的重點「泛型參數預設值(Generic parameter defaults)」。
泛型參數預設值的用法就和 JavaScript 函式中帶入參數預設值的方式一樣,都是用等號(=),在知道泛型的參數也能帶入預設值之後,回過頭來看剛剛的 K extends keyof T = keyof T:
K extends keyof T 的意思是: K 需要滿足 keyof T,也就是說,K 需要是 T 這個物件型別中所包含的屬性 key。K ... = keyof T 的意思就是,如果沒給 K 的話,預設就讓 K 的型別等同於 keyof T,也就是預設的 K 會是所有物件型別中的所有 key。備註:泛型參數預設值並沒有一定要搭配泛型限制(extends)使用。
接著把重點放到 = 的後面:

雖然前幾天曾提過 Omit、Partial 和 Pick 的用法,但可能還是會忘,這時候把握前面提過的原則:「不確定時就帶入實際的型別試試看」:
type A = Omit<Conference, 'year' | 'isAddToCalendar'>;
type B = Pick<Conference, 'year' | 'isAddToCalendar'>;
type C = Partial<Pick<Conference, 'year' | 'isAddToCalendar'>>;
你會發現 A 其實就是把要變成 Optional 的屬性「忽略」掉,只留下不改變的部分:

而 B 就是把要變成 Optional 的屬性「挑出」來:

最後的 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 的部分),改成:

結果會發現,原本我們只帶入一個參數用法的地方跳出錯誤了,因為和 JavaScript 的函式一樣,在沒給參數預設值的情況下,每個參數都需要帶好帶滿才行:

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