本文 AI 產出,尚未審核

TypeScript 進階型別操作 – keyof


簡介

在日常開發中,我們常常需要根據物件的屬性名稱動態地取得或操作資料。
如果僅靠字串或 any 來描述這類情境,TypeScript 的型別安全就會喪失,容易產生執行時錯誤。

keyof 是 TypeScript 提供的 索引型別查詢(indexed type query)運算子,能夠在編譯階段把 物件型別的屬性鍵 轉換成聯合型別(union type)。
掌握 keyof 後,我們可以:

  • 讓函式只接受合法的屬性名稱
  • 建立「屬性名稱」與「屬性值」之間的映射關係
  • 在大型程式碼基底中避免硬編碼字串,提升維護性

本篇將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶出實務應用,幫助你在 TypeScript 專案中自信地使用 keyof。


核心概念

1. keyof 基本語法

type Person = {
  name: string;
  age: number;
  married?: boolean;
};

type PersonKeys = keyof Person;   // "name" | "age" | "married"
  • keyof Person 會遍歷 Person 的所有屬性鍵,產生字面量聯合型別。
  • 可選屬性(married?)同樣會被列入結果,因為屬性鍵仍然存在於型別中。

小技巧:keyof 只會取 直接 的屬性鍵,不會遞迴到父型別(除非使用 extends 產生交叉型別)。


2. 用 keyof 約束函式參數

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, username: "alice", active: true };

const id = getValue(user, "id");           // number
const username = getValue(user, "username"); // string
// const wrong = getValue(user, "email");  // 編譯錯誤:'email' 不是 'user' 的屬性鍵
  • K extends keyof T 讓 key 只能是 obj 真正擁有的屬性名稱。
  • 回傳型別 T[K] 會根據傳入的鍵自動推斷正確的屬性型別,避免手動寫 any。

3. keyof 與映射型別(Mapped Types)

type ReadonlyPerson = {
  readonly [P in keyof Person]: Person[P];
};

type PartialPerson = {
  [P in keyof Person]?: Person[P];
};
  • 透過 keyof 搭配映射型別,我們可以快速產生 只讀、可選 或 其他變形 的版本。
  • 這是建立共用型別工具(utility types)的核心技巧,像是內建的 Partial<T>、Required<T>、Pick<T, K> 等,都依賴 keyof。

4. keyof 與索引簽名(Index Signatures)

type Dictionary = {
  [key: string]: number;
};

type DictKeys = keyof Dictionary; // string | number
  • 當型別包含索引簽名時,keyof 會回傳索引鍵的型別(string、number 或 symbol)。
  • 這讓我們在操作「任意鍵」的字典型別時,仍能保持型別安全。

5. keyof 與條件型別(Conditional Types)

type IsStringKey<T> = keyof T extends string ? true : false;

type Test1 = IsStringKey<Person>; // true,因為所有鍵都是字串字面量
type Test2 = IsStringKey<number[]>; // false,因為陣列的鍵包含 number
  • 利用條件型別,我們可以檢查 keyof 結果是否符合特定條件,進一步實作型別層級的 型別守衛(type guard)。

程式碼範例

以下示範 4 個在實務開發中常見的 keyof 用法,並附上說明。

範例 1:動態表單驗證

type FormValues = {
  email: string;
  password: string;
  rememberMe: boolean;
};

type Validator<T> = {
  [K in keyof T]?: (value: T[K]) => string | undefined;
};

const loginValidator: Validator<FormValues> = {
  email: (v) => (!v.includes("@") ? "必須是有效的 Email" : undefined),
  password: (v) => (v.length < 6 ? "密碼至少 6 個字元" : undefined),
};

function validate<T>(values: T, validator: Validator<T>) {
  const errors: Partial<Record<keyof T, string>> = {};
  for (const key in validator) {
    const fn = validator[key];
    if (fn) {
      const err = fn(values[key]);
      if (err) errors[key] = err;
    }
  }
  return errors;
}

const errors = validate(
  { email: "test", password: "123", rememberMe: false },
  loginValidator
);
// errors => { email: "必須是有效的 Email", password: "密碼至少 6 個字元" }

說明:Validator<T> 透過 keyof T 為每個欄位自動產生對應的驗證函式型別,避免手動同步欄位與驗證規則。


範例 2:安全的屬性更新函式

function updateProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
  return { ...obj, [key]: value };
}

const settings = { theme: "light", fontSize: 14 };
const newSettings = updateProp(settings, "fontSize", 16);
// newSettings => { theme: "light", fontSize: 16 }

// 編譯錯誤:型別不匹配
// updateProp(settings, "theme", 123);

說明:updateProp 只允許合法的屬性鍵與相對應的值型別,避免因字串打錯或型別不符導致的 bug。


範例 3:從物件抽取子集合(Pick 的手寫版)

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type User = {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
};

type UserPreview = MyPick<User, "id" | "name">;
// 等同於: { id: number; name: string; }

說明:透過 keyof 與映射型別,我們可以自行實作類似內建 Pick<T, K> 的工具型別,增進對型別系統的理解。


範例 4:列舉型別與 keyof 的結合

enum Status {
  Pending = "PENDING",
  Success = "SUCCESS",
  Failed = "FAILED",
}

// 取得 enum 的鍵名聯合型別
type StatusKey = keyof typeof Status; // "Pending" | "Success" | "Failed"

function isFinalStatus(key: StatusKey): boolean {
  return key === "Success" || key === "Failed";
}

// 正確使用
isFinalStatus("Success"); // true
// isFinalStatus("Pending"); // false
// isFinalStatus("UNKNOWN"); // 編譯錯誤

說明:keyof typeof Enum 能把列舉的 鍵名 轉成聯合型別,常用於 UI 選單或 API 回傳值的驗證。


常見陷阱與最佳實踐

陷阱 說明 解決方式
**keyof 產生 `string number symbol`**
可選屬性仍被列入 keyof 包含可選屬性,使用時可能忘記檢查 undefined。 在函式內使用 if (key in obj) 或 obj[key] !== undefined 保障安全。
映射型別的分配問題 type A = { [K in keyof B]: ... } 會把所有屬性必定化,可能失去原本的可選性。 使用 -? 或 +? 操作符保留或移除可選性:[K in keyof B]-?: ...。
keyof 與聯合型別的交叉 `keyof (A B)` 只會得到兩者共同的鍵,容易產生意外的限制。
過度使用 any 逃避檢查 為了快速開發,開發者常把 keyof any 替代成 string,失去型別安全。 盡量保留具體的 keyof 結果,或在需要時使用 as const 斷言來鎖定字面量。

最佳實踐:

  1. 使用 as const 鎖定物件字面量,讓 keyof 能得到最精確的聯合型別。
  2. 將 keyof 包裝在泛型工具型別(如 Pick, Partial)中,提高程式碼可重用性。
  3. 搭配 extends keyof 限制函式參數,讓 IDE 能提供自動完成與即時錯誤提示。
  4. 在大型專案中建立共用型別庫,將常用的 keyof 相關工具抽離出來,統一管理。

實際應用場景

1. API 回傳資料的型別保護

當前端收到後端的 JSON,屬性名稱常會隨需求變動。使用 keyof 可以寫出一個 安全的取值函式,保證只取到已知屬性,避免 undefined 帶來的 runtime error。

type ApiResponse = {
  id: number;
  title: string;
  createdAt: string;
};

function safeGet<T>(obj: T, key: keyof T) {
  return obj[key];
}

2. 動態 UI 組件映射

在表格或表單生成器中,我們常根據欄位名稱動態決定渲染哪個元件。keyof 能讓欄位名稱的型別與元件映射表保持同步。

type FieldMap = {
  name: "TextInput";
  age: "NumberInput";
  isAdmin: "Switch";
};

type ComponentName = keyof FieldMap; // "name" | "age" | "isAdmin"

3. Redux / Zustand 狀態管理

在狀態更新時,我們會寫類似 setState(prev => ({ ...prev, [key]: value })) 的程式。使用 keyof 能保證 key 必定是狀態物件的屬性,減少錯誤。

type Store = {
  count: number;
  loading: boolean;
};

function setStore<K extends keyof Store>(key: K, value: Store[K]) {
  // 更新邏輯...
}

總結

  • keyof 是 索引型別查詢 的核心工具,能把物件的屬性鍵轉成聯合型別。
  • 搭配泛型、映射型別、條件型別,我們可以構建出 只讀、可選、Pick、Partial 等多種變形型別,提升程式碼的可維護性與型別安全。
  • 在實務開發中,keyof 常被用於 函式參數限制、動態屬性存取、表單驗證、狀態管理 等情境。
  • 注意索引簽名、可選屬性與聯合型別帶來的陷阱,遵循「盡量保留字面量、使用 as const、封裝成工具型別」的最佳實踐,可讓 keyof 發揮最大效益。

掌握 keyof 後,你將能在 TypeScript 中寫出更嚴謹、彈性且可讀性高的程式碼,讓大型專案的型別維護變得輕鬆自如。祝你在 TypeScript 的世界裡玩得開心,寫出更安全的程式!