TypeScript 映射型別 (Mapped Types)
如果說泛型是型別系統的變數,那「映射型別 (Mapped Types)」就像是型別系統的「迴圈 (Loop)」。
它允許我們遍歷一個型別的所有屬性 (Key),然後對每個屬性進行轉換,最後產生一個新的型別。這就是許多 TypeScript 內建工具型別(如 Partial, Readonly)背後的運作原理。
為什麼需要映射型別?
假設你有一個 User 介面,你想要建立一個新的介面,讓所有欄位都變成 boolean 值(例如用來記錄哪個欄位被修改過)。
沒有映射型別時 (很累):
interface User {
name: string;
age: number;
}
// 手動一個一個寫...
interface UserChanged {
name: boolean;
age: boolean;
}
使用映射型別時 (輕鬆):
type Booleanify<T> = {
[K in keyof T]: boolean;
};
type UserChanged = Booleanify<User>;
// 結果:{ name: boolean; age: boolean; }
基本語法解析
讓我們把語法拆解開來看:
type MappedType<T> = {
[K in keyof T]: NewType;
};
[K in ...]:這就像是for (let K in ...)迴圈。keyof T:取得 T 的所有鍵 (Key) 形成的聯合型別 (例如"name" | "age")。NewType:這是每個屬性對應的新值型別。
簡單來說就是:「對於 T 中的每一個鍵 K,它的值型別都要變成 NewType」。
進階應用:保留或轉換原始型別
通常我們不會把所有東西都變成 boolean,而是會基於「原本的型別 T[K]」做變化。
type MyPartial<T> = {
// 對於每個鍵 K,它的值變成 "原本的值 T[K]" 加上 "undefined" (?)
[K in keyof T]?: T[K];
};
修改屬性修飾符 (+/-)
映射型別最強大的地方在於它可以「批量」修改屬性的修飾符,例如 readonly 或 ? (optional)。
我們可以使用 + (預設,可省略) 或 - 來新增或移除修飾符。
範例:移除 readonly (Mutable)
有時候第三方套件回傳的型別全是 readonly,但我們需要修改它,這時就可以用 -readonly。
interface LockedAccount {
readonly id: string;
readonly name: string;
}
// 建立一個工具型別,把 readonly 移除
type CreateMutable<T> = {
-readonly [K in keyof T]: T[K];
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// 結果:{ id: string; name: string; } -> readonly 不見了!
範例:移除 Optional (Required)
這就是內建 Required<T> 的實作方式:
type MyRequired<T> = {
// 把 ? 移除,變成必填
[K in keyof T]-?: T[K];
};
interface Config {
host?: string;
port?: number;
}
type RequiredConfig = MyRequired<Config>;
// 結果:{ host: string; port: number; }
鍵的重新映射 (Key Remapping) - TypeScript 4.1+
從 TypeScript 4.1 開始,我們可以使用 as 語法在映射過程中修改「鍵的名稱」。這讓我們可以做一些很神奇的事,例如自動產生 get... 或 set... 方法。
範例:自動產生 Getter 方法
我們可以使用 Template Literal Types 來改變鍵的名稱。
interface Person {
name: string;
age: number;
}
type Getters<T> = {
// 把 key 從 "name" 變成 "getName"
// Capitalize 是內建工具,把首字變大寫
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type PersonGetters = Getters<Person>;
// 結果自動產生:
// {
// getName: () => string;
// getAge: () => number;
// }
範例:過濾屬性
如果在 as 後面接 never,該屬性就會被過濾掉。
// 只保留型別為 string 的屬性
type OnlyStringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
id: number; // 這是 number -> 過濾掉
name: string; // 這是 string -> 保留
email: string; // 這是 string -> 保留
isAdmin: boolean; // 這是 boolean -> 過濾掉
}
type StringOnlyUser = OnlyStringProperties<User>;
// 結果:{ name: string; email: string; }
總結
映射型別是 TypeScript 用來「批次製造型別」的工廠。
- 基本語法:
[K in keyof T]: ...就像是型別的for迴圈。 - 存取原值:用
T[K]來拿到原本對應的型別。 - 修改修飾符:用
+或-來控制readonly和?。 - 鍵的重命名:用
as搭配 Template Literal Types 來改 Key 的名字。
學會映射型別,你就不用再手動複製貼上介面,還能寫出像魔術般自動變化的型別邏輯!