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;
};
  1. [K in ...]:這就像是 for (let K in ...) 迴圈。
  2. keyof T:取得 T 的所有鍵 (Key) 形成的聯合型別 (例如 "name" | "age")。
  3. 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 的名字。

學會映射型別,你就不用再手動複製貼上介面,還能寫出像魔術般自動變化的型別邏輯!