跳至主要内容

[Day15] TS:在 Mapped Type 中使用 Template Literal 來改物件型別中的所有 key

Mapped Types

上面這個是今天會提到的內容,如果你已經可以輕鬆看懂,歡迎直接左轉去看我隊友們的精彩文章!

昨天我們已經學到了 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 Types

讓我們來依序拆解並理解這個寫法。

Mapped Type + keyof Type Operator

Mapped Types

首先,從 in 這個關鍵字可以看到這裡用了 Mapped Type,而 [K in keyof T] 的意思就是把 T 這個物件型別的所有 key 取出組成 union types,以這裡來說,如果寫 ToEventHandler<HandledEvent> 的話,這裡的 keyof T 應該就會變成 "click" | "change" | "keyup" | "keydown"

keyof

所以說 [K in keyof T] 就會是用疊代的方式,每次取出 "click" | "change" | "keyup" | "keydown" 中的元素來跑迴圈,可以想像組出來的東西應該會像這樣:

type HandledEvent = {
click: '...';
change: '...';
keyup: '...';
keydown: '...';
};

搭配 as 使用 Template Literal

接著我們看到這裡有 Template Literal 的用法,也就是 `${}` 的用法,並且搭配關鍵字 as 來使用:

Mapped Type

透過 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 中,通常函式的命名會用小寫駝峰的方式,例如,handleClickhandleChange,因此這裡可以在搭配使用在 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

這裡我們把 Mapped Types 跑迴圈是的變數取名為 K,而不論取名是 KP,它都只是個變數名稱,要取名成什麼都可以,但實際上它也不單單只是個名稱,它還可以被拿來被後續使用。

這裡 [...]: 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

參考資料