今天這個範例是來自第三方套件 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