本文 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 Tkey 只能是 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 會回傳索引鍵的型別(stringnumbersymbol)。
  • 這讓我們在操作「任意鍵」的字典型別時,仍能保持型別安全。

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 的世界裡玩得開心,寫出更安全的程式!