[Day17] TS:理解 Pick、Record 的實作
這是我們今天要聊的內容,老樣的,如果你已經可以輕鬆看懂,歡迎直接左轉去看同事 Andy 精彩的文章 — 「前端工程師學習 DevOps 之路」。
在學習了 Mapped Types 後,我們已經把多數要用來操作型別所需的知識補齊了,後面幾天就來看一些實際會用到的實作,包括 TypeScript 中內建以及其他第三方提供的 Utility Types。
今天讓我們先來看 Pick
和 Record
。
Pick
在 TypeScript 內建的 Utility Types 中,有一個 Pick<T, K extends keyof T>
,使用方式很簡單,它可以幫選擇要保留下物件型別中的那些屬性,例如:
type Person = {
firstName: string;
lastName: string;
age: number;
};
type PersonName = Pick<Person, 'firstName' | 'lastName'>;
而 PersonName
的型別就會等同於這樣:
type PersonName = {
firstName: string;
lastName: string;
};
在有了前面關於型別操作的知識後,會發現 Pick
的原始碼其實也蠻單純好理解的:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
我們可以看到 Pick
吃兩個型別參數,一個是 T
它會是物件型別,後面的 K
因為有用的 Day03 提的泛型限制,所以 K
一點要滿足是物件型別 T
中有的屬性 key。
接著 [P in K]
很明顯的是 Mapped Types,以上面提的 PersonName
為例,K
就會是 'firstName' | 'lastName'
,這時候的 [P in 'firstName' | 'lastName']
讀者們應該可以預想到最後出來的型別其物件型別的屬性 key 會長這樣:
{
firstName: '...';
lastName: '...';
}
最後看到屬性 value 的部分是 T[P]
,意思也就是,什麼都不做,原本物件屬性值的型別是什麼就直接拿來用,因此最後 PersonName
的型別會長這樣:
type PersonName = {
firstName: string;
lastName: string;
};
Record
接著我們來看一下 Record<Keys, Type>
。前面我們有提過 Mapped Types 是比較有限制的 Index Signatures,也就是 Mapped Types 是 Index Signatures 的子集合,而 Record
這個 Utility Type 同樣是基於 Mapped Types 寫出來的,先來簡單看一下它的用法。
在沒有 Record 之前
假設現在需要建立一個物件型別,它的 key 希望符合 ConferenceName
、value 符合型別 Conference
:
type ConferenceName = 'ModernWeb' | 'MOPCON' | 'JSDOC' | '{Laravel x Vue}';
type Conference = {
name: string;
year: number;
isAddToCalendar: boolean;
website: string;
};
前面 Day14 曾提過,因為 Index Signatures 的特性,並沒有辦法直接寫:
type ConferenceIndexSignatures = {
[K: ConferenceName]: Conference;
};
TypeScript 會報錯,並建議我們用 Mapped Type:
於是用 Mapped Types 可以寫成這樣:
type ConferenceMap = {
[P in ConferenceName]: Conference;
};
接著我們把它改成更泛用的形式試試看,先把 ConferenceName
抽成泛型:
// 把 ConferenceName 變成泛型
type ToConferenceMap<K> = {
[P in K]: Conference;
};
這時候你會看到 TypeScript 報錯:
之所以會有這個錯誤的原因是 K
沒有辦法被保證一定能把疊代,如果 K 被帶入物件型別的話,[P in K]
就會壞掉,因此 TypeScript 說 K
應該只能是 string | number | symbol
,而這其實也就是 keyof any
的意思,因此可以透過泛型限制來限制使用者可以帶入的 K
:
type ToConferenceMap<K extends keyof any> = {
[P in K]: Conference;
};
接著我們來把物件型別的屬性值也抽成泛型:
type ToConferenceMap<K extends keyof any, T> = {
[P in K]: T;
};
有沒有發現剛剛寫的 ToConferenceMap
變的更泛用了!現在我們可以使用剛剛自己寫的這個 Utility Type 來產生 ConferenceMap
:
type ConferenceName = 'ModernWeb' | 'MOPCON' | 'JSDC' | '{Laravel x Vue}';
type Conference = {
name: string;
year: number;
isAddToCalendar: boolean;
website: string;
};
type ToConferenceMap<K extends keyof any, T> = {
[P in K]: T;
};
type ConferenceMap = ToConferenceMap<ConferenceName, Conference>;
如此就可以得到我們想要的物件型別:
更重要的是,其實我們已經寫出了官方提供的 Record
了!
使用 Record 與原始碼
在 TypeScript 中,Record<Keys, Type>
可以讓開發者方便定義物件型別中屬性 key 和 value 的型別,而你會發現用 Record 建立出來的型別,和用剛剛我們寫的 ToConferenceMap
的效果一樣:
// 兩個建立出來的型別是一樣的
type ConferenceMap = ToConferenceMap<ConferenceName, Conference>;
type ConferenceRecord = Record<ConferenceName, Conference>;
回過頭來看 Record
的原始碼:
// Construct a type with a set of properties K of type T
type Record<K extends keyof any, T> = {
[P in K]: T;
};
有沒有發現和我們剛剛寫的 ToConferenceMap
其實是一樣的呢?
建立自己的 Utility Types
讀者們應該慢慢可以發現,要寫出 Utility Types 比想像中的簡單,可以先根據實際的情況撰寫出想要的結果後,再把可以抽成參數的部分變成泛型,最後就可以建立出更泛用的 Utility Types。
當然也是有一些很複雜的 Utility Types 可能沒辦法這麼簡單就寫的出來,但重要的是能夠透過正確的斷句,先求能夠看懂和理解。
範例程式碼
https://tsplay.dev/WJ8E5N @ TypeScript Playground
參考資料
- Utility Types @ TypeScript > Reference