TypeScript 工具型別:Record<K, T>
簡介
在日常的前端開發或 Node.js 專案中,我們常需要把 鍵(key)與 值(value)之間的關係固定下來,讓程式碼在編譯階段就能捕捉到錯誤、保證資料結構的一致性。Record<K, T> 正是 TypeScript 為此提供的 工具型別(Utility Type)之一,它可以在編譯時動態產生一個物件型別,讓每個指定的鍵都對應到相同的值型別 T。
使用 Record 的好處包括:
- 可讀性提升:一眼就能看出「這是一個以 X 為鍵、Y 為值的映射」
- 自動補全:IDE 能根據型別提供鍵名與值型別的完整提示
- 錯誤防護:錯誤的鍵或不符合型別的值會在編譯階段被捕捉
下面我們將從概念、語法、實作範例一路深入,最後說明常見陷阱與最佳實踐,幫助你在實務上有效運用 Record。
核心概念
1. Record 的基本語法
type Record<K extends keyof any, T> = {
[P in K]: T;
};
K必須是 可作為鍵的型別(string | number | symbol)的子集合。T是 所有鍵所對應的值型別,可以是任意型別,甚至是另一個物件型別。
簡單來說,Record<'a' | 'b', number> 會被展開成:
{
a: number;
b: number;
}
2. 為什麼不直接寫介面(interface)?
如果鍵的集合是 固定且少量,手動寫介面當然沒問題;但當鍵是 來自變數、Enum、或是 API 回傳的資料 時,手寫會變得冗長且易出錯。Record 讓我們可以 以型別層級的方式 把這些動態鍵映射到同一型別,保持程式碼的 DRY(Don't Repeat Yourself)原則。
3. Record 與 Partial、Required 的關係
Record 只負責 鍵和值的對應,不會改變屬性的必填/選填狀態。若需要把所有屬性變成可選或必填,可結合其他工具型別:
type OptionalRecord<K extends keyof any, T> = Partial<Record<K, T>>;
type RequiredRecord<K extends keyof any, T> = Required<Record<K, T>>;
程式碼範例
以下示範 5 個常見且實用的 Record 用法,並加入詳細註解說明。
範例 1:建立字典(Dictionary)
// 需求:一個以語言代碼為鍵、語系文字為值的字典
type LocaleDict = Record<'en' | 'zh-TW' | 'ja', string>;
const messages: LocaleDict = {
en: 'Hello',
'zh-TW': '哈囉',
ja: 'こんにちは',
};
// 編譯錯誤示範(缺少鍵或值型別不符)
// const wrong: LocaleDict = { en: 'Hi' }; // ❌ Property 'zh-TW' is missing
// const wrong2: LocaleDict = { en: 123, 'zh-TW': '哈囉', ja: 'こんにちは' }; // ❌ Type 'number' is not assignable to type 'string'
重點:
Record讓鍵的完整性在編譯期即被驗證,避免遺漏或拼寫錯誤。
範例 2:把 Enum 轉成映射表
enum Role {
Admin = 'ADMIN',
Editor = 'EDITOR',
Viewer = 'VIEWER',
}
// 每個角色對應的權限說明
type RoleDescription = Record<Role, string>;
const roleDesc: RoleDescription = {
[Role.Admin]: '擁有全部權限',
[Role.Editor]: '可編輯內容',
[Role.Viewer]: '只能檢視',
};
使用 Enum 作為
K,可確保所有列舉成員都被映射,且在新增 Enum 成員時會自動觸發型別錯誤提醒。
範例 3:從外部 API 動態生成 Record
假設我們從後端取得的欄位清單是一個字串陣列,想要產生一個「欄位名稱 → 欄位型別」的映射表:
// 假資料:從 API 取得的欄位名稱
const apiFields = ['id', 'name', 'age'] as const;
// 透過 typeof + typeof[] 取得字面量型別
type ApiField = typeof apiFields[number]; // 'id' | 'name' | 'age'
// 以 Record 生成欄位型別映射(全部為 string,實務上可改成更精細的型別)
type ApiRecord = Record<ApiField, string>;
const userRecord: ApiRecord = {
id: 'U001',
name: 'Alice',
age: '30',
};
技巧:使用
as const讓字串陣列成為 字面量型別,再配合Record產生動態且安全的物件型別。
範例 4:深層結構的 Record(巢狀映射)
有時候我們需要 鍵 → 另一個映射表 的結構,例如多語系內容:
type Translations = Record<'en' | 'zh-TW' | 'ja', Record<string, string>>;
const i18n: Translations = {
en: { welcome: 'Welcome', logout: 'Log out' },
'zh-TW': { welcome: '歡迎', logout: '登出' },
ja: { welcome: 'ようこそ', logout: 'ログアウト' },
};
// 取得日文的「logout」文字
const logoutJa = i18n.ja.logout; // 'ログアウト'
透過 巢狀
Record,我們可以在同一型別裡定義多層次的映射關係,保持結構一致性。
範例 5:結合 Partial 實作「可選設定」
假設我們有一個函式接受設定物件,所有設定皆可選,但若提供則必須符合特定型別:
type ConfigKey = 'timeout' | 'retry' | 'verbose';
type ConfigValue = number | boolean;
type Config = Partial<Record<ConfigKey, ConfigValue>>;
function init(config: Config = {}) {
const timeout = (config.timeout ?? 3000) as number;
const retry = (config.retry ?? 2) as number;
const verbose = (config.verbose ?? false) as boolean;
// ... 其他初始化邏輯
}
// 正確使用
init({ timeout: 5000, verbose: true });
// 編譯錯誤示範(型別不符)
// init({ retry: 'three' }); // ❌ Type 'string' is not assignable to type 'ConfigValue'
關鍵:
Partial<Record<...>>讓所有鍵變成可選,同時仍保留 值的型別檢查。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
鍵的型別不符合 keyof any |
使用自訂類別或物件作為 K 會報錯。 |
只使用 string、number、symbol 或其聯集;若需要物件屬性,可先提取其鍵型別 keyof typeof obj。 |
忘記 as const 而失去字面量型別 |
直接使用普通陣列會被推斷為 string[],導致 Record<string, …> 而非預期的聯集。 |
在字面量陣列後加上 as const,或使用 const enum。 |
| 值型別過於寬鬆 | Record<string, any> 失去型別檢查的意義。 |
盡量使用具體型別,必要時結合泛型或條件型別限制。 |
**與 Partial/Required 混用產生不必要的 ` |
undefined`** | Partial<Record<…>> 會把每個屬性變成 `T |
巢狀 Record 的深層型別推斷過於複雜 |
多層 Record 可能導致編譯時間變長,IDE 補全卡頓。 |
若層級超過 2~3 層,考慮抽離成獨立型別或使用 interface 來提升可讀性。 |
最佳實踐小結
- 先定義鍵的字面量型別(
type Keys = 'a' | 'b'或enum),再套用Record。 - 盡量避免使用
any,把值型別寫得具體一點。 - 結合其他工具型別(
Partial、Readonly、Pick)以滿足需求。 - 在大型專案中,將常用的
Record型別抽成type或interface,保持一致性。 - 使用
as const讓字面量陣列/物件保留精確型別,讓Record發揮最大威力。
實際應用場景
| 場景 | 為什麼適合使用 Record |
|---|---|
| 表單驗證規則 | 把欄位名稱映射到驗證函式:Record<FormField, (value: any) => boolean> |
| 多語系 UI 文案 | 語言代碼 → 翻譯字串的字典,保證每個語言都有對應文字。 |
| 權限管理 | 角色代碼 → 權限陣列或說明文字,確保所有角色都有設定。 |
| API 回傳的固定欄位 | 例如:`Record<'id' |
| 動態產生的設定檔 | 從外部 JSON 讀取鍵名列表,使用 as const + Record 生成安全的設定介面。 |
總結
Record<K, T> 是 TypeScript 提供的 高階工具型別,它讓我們能在編譯階段快速、精確地描述「鍵 → 同型別值」的映射關係。
- 透過 字面量型別、Enum 或
as const,我們可以把動態或外部取得的鍵安全地套用到Record。 - 結合
Partial、Required、Readonly等其他工具型別,可彈性控制屬性的必填與可變性。 - 在實務開發中,
Record常被用於 字典、權限表、多語系翻譯、設定檔 等場景,大幅提升程式碼的可讀性與錯誤防護。
掌握 Record 的使用方式後,你將能更自信地設計資料結構,讓 TypeScript 的型別系統真正成為開發的「安全網」。祝你在 TypeScript 的世界裡寫出更乾淨、更可靠的程式碼! 🚀