[Day15] TS:在 Mapped Type 中使用 Template Literal 來改物件型別中的所有 key
上面這個是今天會提到的內容,如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!
昨天我們已經學到了 Mapped Types 的精華,就是可以在 Index Signatures 中使用 in
來疊代 in
後面的所有元素,便可以透過 Mapped Type 修改物件型別中所有屬性值的型別。因此,可以把原本的 SupportedEvent
進行修改 :
type SupportedEvent = {
click: string;
change: string;
keyup: string;
keydown: string;
};
透過 HandledEvent
這個 Utility Type 進行轉換後,產生出新的型別:
type HandledEvent = {
[K in keyof SupportedEvent]: () => void;
};
更好的方式還可以把 Supported Event
變成一個泛型的參數,讓它變成 Utility Type,像是這樣,如此 MappedValuesToFunction
就可以變的更泛用:
type MappedValuesToFunction<T> = {
[K in keyof T]: () => void;
};
type HandledEvent = MappedValueToFunction<SupportedEvent>;
修改物件型別的所有 key
從昨天的內容中,我們已經知道如何透過 Mapped Types 「一次修改所有 SupportedEvent
屬性值的型別」,最後得到的 HandledEvent
會是:
type HandledEvent = {
click: () => void;
change: () => void;
keyup: () => void;
keydown: () => void;
};
如果現在我們不是要修改物件型別中屬性值的型別,而是想要修改屬性 key 的名稱時,可以怎麼做呢?舉例來說,我們想要 根據 HandledEvent
,透過某個 Utility Type 後可以產生出像這樣的型別:
type EventHandler = {
handleClick: () => void;
handleChange: () => void;
handleKeyup: () => void;
handleKeydown: () => void;
};
這時候就可以用到今天要提的這個 Utility Type:
讓我們來依序拆解並理解這個寫法。
Mapped Type + keyof Type Operator
首先,從 in
這個關鍵字可以看到這裡用了 Mapped Type,而 [K in keyof T]
的意思就是把 T
這個物件型別的所有 key 取出組成 union types,以這裡來說,如果寫 ToEventHandler<HandledEvent>
的話,這裡的 keyof T
應該就會變成 "click" | "change" | "keyup" | "keydown"
:
所以說 [K in keyof T]
就會是用疊代的方式,每次取出 "click" | "change" | "keyup" | "keydown"
中的元素來跑迴圈,可以想像組出來的東西應該會像這樣:
type HandledEvent = {
click: '...';
change: '...';
keyup: '...';
keydown: '...';
};
搭配 as 使用 Template Literal
接著我們看到這裡有 Template Literal 的用法,也就是 `${}`
的用法,並且搭配關鍵字 as
來使用:
透過 as
後面接字串型別的方式,就可以讓我們達到修改屬性 key
的目的。
現在為了方便理解,我們先把 Template Literal 中的 Capitalize
拿掉:
type ToEventHandler<T> = {
[K in keyof T as `handle${string & K}`]: T[K];
};
這時候應該會看到經過這個 Utility Type 後跑出來的型別會是:
type EventHandler = ToEventHandler<HandledEvent>;
// 預期 EventHandler 會長這樣
type EventHandler = {
handleclick: '...';
handlechange: '...';
handlekeyup: '...';
handlekeydown: '...';
};
這裡我們已經成功透過 Mapped Type 搭配 as
和 Literal Template 的使用,成功把物件型別的 key 進行了轉換,讓每個 key 的最前面都多了 handle...
的前綴。
但因為在 JavaScript 中,通常函式的命名會用小寫駝峰的方式,例如,handleClick
、handleChange
,因此這裡可以在搭配使用在 Day12 時曾經提到的 Intrinsic String Manipulation Types ,透過 Capitalize
來將字串型別的第一個字轉成英文大寫,因此如果改會原本的寫法 as `handle${Capitalize<string & K>}`
後,產生出來的 key 的名稱就會是我們想要的以 handle
作為開頭的 key:
type EventHandler = ToEventHandler<HandledEvent>;
// 預期 EventHandler 會長這樣
type EventHandler = {
handleClick: '...';
handleChange: '...';
handleKeyup: '...';
handleKeydown: '...';
};
Mapped Type 中的 K (key) 是可以拿來使用的
來看最後一個部分:
這裡我們把 Mapped Types 跑迴圈是的變數取名為 K
,而不論取名是 K
或 P
,它都只是個變數名稱,要取名成什麼都可以,但實際上它也不單單只是個名稱,它還可以被拿來被後續使用。
這裡 [...]: T[K]
中 :
後面放的就是這個屬性值的型別,我們知道 T
就是我們帶進去的物件型別,而 K
其實就是沒有次跑迴圈時的 key
,因此 T[K]
就是我們在 Day05 曾提過的 Indexed Access Types,意思就是直接把該屬性值原本的型別取出來就好。
總結
現在總結來看,應該就可以知道怎麽透過 Mapped Type 搭配 as
和 Template Literal 來修改物件型別中的屬性名稱:
type SupportedEvent = {
click: string;
change: string;
keyup: string;
keydown: string;
};
type MappedValuesToFunction<T> = {
[K in keyof T]: () => void;
};
type HandledEvent = MappedValuesToFunction<SupportedEvent>;
type ToEventHandler<T> = {
[K in keyof T as `handle${Capitalize<string & K>}`]: T[K];
};
type EventHandler = ToEventHandler<HandledEvent>;
這裡最終的 EventHandler
就會是:
type EventHandler = {
handleClick: () => void;
handleChange: () => void;
handleKeyup: () => void;
handleKeydown: () => void;
};
範例程式碼
https://tsplay.dev/mAVZRW @ TypeScript Playground
參考資料
- Mapped Types @ TypeScript > Type Manipulation