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 斷言來鎖定字面量。 |
最佳實踐:
- 使用
as const鎖定物件字面量,讓keyof能得到最精確的聯合型別。 - 將
keyof包裝在泛型工具型別(如Pick,Partial)中,提高程式碼可重用性。 - 搭配
extends keyof限制函式參數,讓 IDE 能提供自動完成與即時錯誤提示。 - 在大型專案中建立共用型別庫,將常用的
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 的世界裡玩得開心,寫出更安全的程式!