TypeScript 教學:深入了解 Record<string, number> 的用法
簡介
在日常的前端開發中,我們常需要管理「鍵值對」形式的資料,例如統計各類別的次數、維護設定檔或是處理 API 回傳的字典資料。TypeScript 為了讓這類情境更安全、更易維護,提供了內建的泛型型別 Record<Keys, Type>,讓開發者可以在編譯階段即得到鍵與值的正確類型檢查。
本單元「陣列與集合(Arrays & Collections)」的 範例:Record<string, number> 用法,將帶你一步步了解:
Record為何是「映射」型別的好幫手。- 如何在不同情境下使用
Record<string, number>,從簡單的計數器到複雜的資料合併。 - 常見的陷阱與最佳實踐,避免在大型專案中踩雷。
掌握這個概念後,你將能以更結構化的方式處理「字串鍵 → 數字值」的資料,提升程式的可讀性與可靠度。
核心概念
什麼是 Record<Keys, Type>?
Record 是 TypeScript 標準庫中的泛型工具型別,語法如下:
type Record<Keys extends keyof any, Type> = {
[K in Keys]: Type;
};
Keys:鍵的集合,可以是字面量聯合類型 ('a' | 'b')、string、number等。Type:對應每個鍵的值型別。
簡單來說,Record<string, number> 代表「一個以 任意字串 為鍵、數字 為值的物件」。這與以下等價寫法相同:
type MyMap = {
[key: string]: number;
};
但使用 Record 的好處是 語意更清晰,且在需要動態鍵集合時(如從泛型參數取得鍵)更具彈性。
為什麼選擇 Record<string, number>?
| 場景 | 常見寫法 | 使用 Record 的好處 |
|---|---|---|
| 統計字串出現次數 | const count: {[k: string]: number} = {}; |
明確表達「字串 → 數字」的映射關係 |
| API 回傳的字典資料 | interface Resp { [code: string]: number; } |
可直接利用 Record 產生型別,避免重複寫介面 |
| 動態產生鍵 | type Keys = keyof typeof source; type Map = Record<Keys, number>; |
以泛型自動推導鍵集合,保證型別安全 |
程式碼範例
以下示範 5 個實用案例,從最基礎的建立到進階的合併與型別守衛,並在每段程式碼中加入詳細註解。
1. 基本建立與存取
// 建立一個 Record<string, number>,初始為空物件
const wordCount: Record<string, number> = {};
// 新增或更新鍵值
wordCount["apple"] = 3;
wordCount["banana"] = 5;
// 讀取值時,若鍵不存在會得到 undefined(需自行處理)
const appleCount = wordCount["apple"]; // 3
const orangeCount = wordCount["orange"]; // undefined
重點:
Record<string, number>允許任意字串鍵,但在讀取時仍需考慮undefined的情況。
2. 統計字串出現次數(常見的文字計數器)
function tally(words: string[]): Record<string, number> {
const result: Record<string, number> = {};
for (const w of words) {
// 若鍵已存在則累加,否則初始化為 1
result[w] = (result[w] ?? 0) + 1;
}
return result;
}
// 範例
const data = ["apple", "banana", "apple", "orange", "banana", "apple"];
const counts = tally(data);
console.log(counts); // { apple: 3, banana: 2, orange: 1 }
使用 空值合併運算子 (
??) 可以避免undefined帶來的NaN問題。
3. 合併多個 Record<string, number>(資料彙總)
function mergeRecords(
records: Record<string, number>[]
): Record<string, number> {
const merged: Record<string, number> = {};
for (const rec of records) {
for (const key in rec) {
merged[key] = (merged[key] ?? 0) + rec[key];
}
}
return merged;
}
// 範例
const r1 = { apple: 2, banana: 1 };
const r2 = { apple: 1, orange: 4 };
const r3 = { banana: 3, grape: 2 };
const total = mergeRecords([r1, r2, r3]);
console.log(total); // { apple: 3, banana: 4, orange: 4, grape: 2 }
此範例展示 迭代
Record的方式(for...in),以及如何安全地累加數值。
4. 使用泛型產生特定鍵集合的 Record
type Fruit = "apple" | "banana" | "orange";
/**
* 產生只允許 Fruit 鍵的 Record,值仍然是 number
*/
type FruitCount = Record<Fruit, number>;
const fruitBag: FruitCount = {
apple: 5,
banana: 2,
orange: 0,
};
// 編譯階段會檢查缺少或多餘的鍵
// fruitBag["grape"] = 1; // ❌ TypeScript 會報錯
好處:透過字面量聯合型別限制鍵的範圍,讓錯誤在編譯時即被捕捉。
5. 型別守衛:確保鍵存在再使用
function getCount(
map: Record<string, number>,
key: string
): number {
// 型別守衛:檢查 map 是否真的有此鍵
if (Object.prototype.hasOwnProperty.call(map, key)) {
return map[key]; // 此時 TypeScript 仍認為可能是 undefined,需要斷言
}
// 若不存在,回傳 0(或拋出錯誤)
return 0;
}
// 範例
const stats = { apple: 4, banana: 2 };
console.log(getCount(stats, "apple")); // 4
console.log(getCount(stats, "orange")); // 0
使用
hasOwnProperty或key in map可以避免undefined帶來的 runtime 問題,並讓程式更具可預測性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 鍵不存在時直接取值 | record[key] 可能回傳 undefined,在做算術運算時會產生 NaN。 |
使用 空值合併 (??) 或 型別守衛(hasOwnProperty)先檢查。 |
誤用 any |
若把 Record<string, any> 當成「萬用」型別,會失去 TypeScript 的型別安全。 |
盡量具體化值的型別,例如 Record<string, number>,或使用 映射型別 (Mapped Types) 進一步限制。 |
| 鍵的類型過寬 | Record<string, number> 允許任意字串,可能導致拼寫錯誤不易發現。 |
若鍵集合已知,改用 字面量聯合型別(`Record<'apple' |
| 深層合併失敗 | Object.assign 只做淺層合併,若值本身是物件會被覆蓋。 |
針對數值累加使用自訂合併邏輯(如範例 3),或使用 lodash.mergeWith 等工具。 |
使用 for...in 時忘記過濾原型屬性 |
for...in 會遍歷原型鏈上的屬性,可能誤把非資料鍵算進去。 |
加上 if (Object.prototype.hasOwnProperty.call(obj, key)) 來過濾。 |
最佳實踐小結
- 明確宣告鍵的範圍:若鍵集合固定,使用聯合型別避免拼寫錯誤。
- 安全存取:總是檢查
undefined,或使用?? 0作為預設值。 - 避免
any:保持值的型別具體,提升編譯期檢查的效益。 - 使用泛型抽象:在函式或類別中接受
Record<K, number>,讓 API 更彈性且安全。 - 寫測試:對於合併或累加的邏輯,加入單元測試確保不會因為鍵遺漏而產生錯誤。
實際應用場景
前端表單驗證計數
- 針對使用者輸入的欄位名稱統計錯誤次數,
Record<string, number>可直接映射欄位 → 錯誤次數,方便在 UI 上顯示紅點或提示訊息。
- 針對使用者輸入的欄位名稱統計錯誤次數,
API 回傳字典資料
- 某些 RESTful API 會回傳類似
{ "USD": 1.0, "EUR": 0.85, "JPY": 110.5 }的匯率表,使用Record<string, number>定義回傳型別,讓呼叫端在編譯時即知道每個幣別的值是數字。
- 某些 RESTful API 會回傳類似
遊戲分數排行榜
- 玩家名稱(字串)對應分數(數字),使用
Record<string, number>可以快速更新、查詢、排序,且在多人同時寫入時仍保持型別安全。
- 玩家名稱(字串)對應分數(數字),使用
日誌統計與分析
- 伺服器端收集不同類別的請求次數(例如
"GET": 1200, "POST": 300),Record<string, number>能作為中間資料結構,之後再轉成圖表或報表。
- 伺服器端收集不同類別的請求次數(例如
設定檔與功能開關
- 某些功能的權重或啟用次數以字串鍵儲存,使用
Record<string, number>可以在 TypeScript 中直接驗證設定檔的結構,避免 JSON 錯誤導致程式崩潰。
- 某些功能的權重或啟用次數以字串鍵儲存,使用
總結
Record<string, number> 是 TypeScript 中處理「字串鍵 → 數字值」映射的核心工具。透過本篇文章,我們了解了:
Record的基本語法與意圖。- 為何在統計、合併、API 回傳等情境中選擇
Record<string, number>。 - 五個實務範例,從建立、累加、合併到型別守衛的完整流程。
- 常見陷阱(
undefined、鍵過寬、any)與對應的 最佳實踐。 - 真實的開發場景,說明此型別如何提升程式安全性與可維護性。
掌握這些概念後,你將能在 陣列與集合 的課程中,靈活運用 Record 來管理各式字典資料,寫出更可靠、易讀、易維護的 TypeScript 程式碼。祝你在實務開發中玩得開心、寫得順手! 🚀