本文 AI 產出,尚未審核
TypeScript 泛型工具型別:keyof、typeof、in
簡介
在大型前端或 Node.js 專案中,型別安全是維持程式碼品質的關鍵。TypeScript 的泛型讓我們可以寫出彈性且可重用的函式或類別,而配合 keyof、typeof、in 這三個工具型別,則能把「值」與「型別」之間的關係寫得更精確、更自動化。
keyof讓我們從物件型別抽取所有屬性名稱,形成字面量聯合型別。typeof把 變數的實際值 轉換成型別,避免手動重複寫型別宣告。in則是映射型別(Mapped Types)的核心語法,能以動態方式產生新型別。
掌握這三個工具型別,能在 編譯期即捕捉錯誤、減少冗餘程式碼,提升開發效率與可維護性。以下將逐一說明概念、範例、常見陷阱與實務應用。
核心概念
1. keyof:從型別抽取鍵名
keyof T 會回傳型別 T 所有屬性的字面量聯合型別。例如:
interface User {
id: number;
name: string;
email?: string;
}
// 取得 User 的所有鍵名
type UserKey = keyof User; // "id" | "name" | "email"
為什麼有用?
- 限制函式參數:只接受物件中實際存在的鍵,防止打錯字。
- 動態存取屬性:搭配泛型與索引存取 (
obj[key]) 時,TS 能正確推斷回傳型別。
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const alice: User = { id: 1, name: "Alice" };
const email = getProp(alice, "email"); // email 型別是 string | undefined
2. typeof:把值變成型別
typeof 與 JavaScript 的 typeof 不同,它在型別層級上使用,用來取得變數或常數的型別。
const API_ENDPOINT = "https://api.example.com/v1";
type Endpoint = typeof API_ENDPOINT; // string literal type: "https://api.example.com/v1"
應用情境
- 共享常數與型別:避免手動寫
type Endpoint = string;,確保常數改名時型別同步更新。 - 從函式返回值推斷型別:
function createConfig() {
return {
host: "localhost",
port: 8080,
secure: false,
};
}
type Config = ReturnType<typeof createConfig>; // { host: string; port: number; secure: boolean; }
3. in:映射型別(Mapped Types)
in 用在 型別映射,可以把一個鍵集合(如 keyof)映射成新型別。最常見的寫法是:
type Partial<T> = {
[P in keyof T]?: T[P];
};
範例:將所有屬性設為只讀
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// readonly email?: string;
// }
結合 keyof、typeof 與條件型別
const permissions = {
read: "READ",
write: "WRITE",
delete: "DELETE",
} as const;
type PermissionKey = keyof typeof permissions; // "read" | "write" | "delete"
type PermissionMap = {
[K in PermissionKey]: boolean;
};
const userPermissions: PermissionMap = {
read: true,
write: false,
delete: false,
};
程式碼範例彙總
以下提供 5 個實用範例,示範三個工具型別的結合使用,並附上註解說明。
// ------------------- 範例 1:安全的屬性存取 -------------------
interface Product {
id: number;
name: string;
price: number;
}
// 泛型函式,只允許存取 Product 真正的鍵名
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach((k) => (result[k] = obj[k]));
return result;
}
const p: Product = { id: 1, name: "筆記本", price: 1200 };
const mini = pick(p, ["id", "name"]); // { id: 1, name: "筆記本" }
// ---------------------------------------------------------
// ------------------- 範例 2:從常數自動產生型別 -------------------
const STATUS = {
SUCCESS: "success",
FAIL: "fail",
PENDING: "pending",
} as const;
// 使用 typeof + keyof 產生合法的狀態字串型別
type Status = typeof STATUS[keyof typeof STATUS]; // "success" | "fail" | "pending"
function setStatus(s: Status) {
console.log(`Current status: ${s}`);
}
setStatus("success"); // 正確
// setStatus("unknown"); // 編譯錯誤
// ---------------------------------------------------------
// ------------------- 範例 3:動態建立 DTO -------------------
type ApiResponse<T> = {
data: T;
error?: string;
};
type UserDTO = {
userId: number;
username: string;
};
type UserResponse = ApiResponse<UserDTO>;
// 等同於 { data: { userId: number; username: string }; error?: string; }
// ------------------- 範例 4:條件映射型別 -------------------
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email?: string | null; }
// ------------------- 範例 5:深層只讀(Recursive Readonly) -------------------
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
type Config = {
api: {
endpoint: string;
timeout: number;
};
debug: boolean;
};
type ImmutableConfig = DeepReadonly<Config>;
// 所有層級都變成 readonly,編譯期防止意外變更
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
**keyof 產生 `string |
number | symbol`** |
typeof 失去字面量類型 |
直接 typeof 變數會得到「寬鬆」的型別(如 string),除非使用 as const 斷言。 |
為常數加上 as const,或使用 const enum(在需要編譯期常量時)。 |
映射型別遺失可選屬性 (?) |
in 產生的新型別預設會把所有屬性變為必填。 |
在映射時保留可選性:[P in keyof T]-?: T[P](-? 移除可選)或 +?(保留)視需求而定。 |
| 遞迴映射型別造成「過深」錯誤 | 深層遞迴(如 DeepReadonly)在極端情況下會觸發 TypeScript 的遞迴深度限制。 |
限制遞迴深度或使用 any/unknown 作為終止條件;在 tsconfig.json 中調整 maxDepth(--maxNodeModuleJsDepth 只針對模組)較少見。 |
使用 in 時忘記加上索引簽名 ([key: string]) |
若映射的鍵集合不完整,TypeScript 仍會要求符合原型別的所有鍵。 | 使用 Partial<T> 或 Record<string, T> 來彈性處理未知鍵。 |
最佳實踐
- 盡量使用
as const讓字面量保持最精確的型別,配合typeof可產生「字面量型別」。 - 在泛型約束中加入
keyof,保證傳入的鍵一定是合法屬性,避免any的不安全。 - 利用條件型別 (
T extends ... ? ... : ...) 結合in,寫出更具彈性的工具型別(如Partial,Required,Pick)。 - 保持型別與實作同步:若常數或 API 回傳結構改變,只需要更新一次(使用
typeof或ReturnType),所有相關型別自動更新。 - 在大型專案中建立共用的型別工具檔(如
type-utils.ts),集中管理PartialDeep,Mutable,DeepPartial等自訂型別,提升可維護性。
實際應用場景
| 場景 | 使用的工具型別 | 為什麼適合 |
|---|---|---|
| 表單驗證:根據 API 回傳的欄位自動產生表單型別 | keyof + 映射型別 (in) |
只要 API 定義變動,表單型別自動更新,減少手動同步的錯誤。 |
Redux / Zustand 狀態管理:從 initialState 推斷狀態型別 |
typeof + keyof |
initialState 只寫一次,型別自動推斷,避免 state 結構不一致。 |
| 多語系文字資源:從語言檔案生成合法的 key 型別 | typeof + keyof + in |
確保 t('some.key') 的 key 必須存在於語言檔,編譯期即捕捉錯字。 |
| API 客戶端:根據後端 Swagger JSON 動態產生請求函式的參數型別 | typeof + 映射型別 |
只要 Swagger 更新,型別自動重新產生,客戶端永遠與後端同步。 |
| 權限系統:從一組權限常數產生角色對應的布林映射 | typeof + keyof + in |
只要新增或刪除權限,所有角色的型別都會即時更新,避免遺漏檢查。 |
總結
keyof、typeof、in 是 TypeScript 泛型工具型別 中最基礎、也最威力強大的三把鑰匙。透過:
keyof抽取 屬性鍵,讓函式參數、映射型別更安全;typeof把 值 直接轉成 型別,避免重複宣告與同步問題;in建立 映射型別,可動態產生 Partial、Readonly、Deep 等高階型別,
我們能在 編譯期即捕捉錯誤、減少冗餘程式碼,並在大型專案中保持 型別與實作的一致性。只要遵守前述的最佳實踐,並留意常見陷阱,就能把 TypeScript 的型別系統發揮到最大效能,寫出更可靠、可維護的程式碼。
實務提醒:在日常開發中,先從
as const+typeof建立「常數型別」開始,逐步擴展到keyof+in的映射型別,讓型別安全成為開發流程的自然一環。祝你在 TypeScript 的世界裡玩得開心、寫得乾淨!