本文 AI 產出,尚未審核

TypeScript 教學:深入了解 Record<string, number> 的用法


簡介

在日常的前端開發中,我們常需要管理「鍵值對」形式的資料,例如統計各類別的次數、維護設定檔或是處理 API 回傳的字典資料。TypeScript 為了讓這類情境更安全、更易維護,提供了內建的泛型型別 Record<Keys, Type>,讓開發者可以在編譯階段即得到鍵與值的正確類型檢查。

本單元「陣列與集合(Arrays & Collections)」的 範例:Record<string, number> 用法,將帶你一步步了解:

  1. Record 為何是「映射」型別的好幫手。
  2. 如何在不同情境下使用 Record<string, number>,從簡單的計數器到複雜的資料合併。
  3. 常見的陷阱與最佳實踐,避免在大型專案中踩雷。

掌握這個概念後,你將能以更結構化的方式處理「字串鍵 → 數字值」的資料,提升程式的可讀性與可靠度。


核心概念

什麼是 Record<Keys, Type>

Record 是 TypeScript 標準庫中的泛型工具型別,語法如下:

type Record<Keys extends keyof any, Type> = {
  [K in Keys]: Type;
};
  • Keys:鍵的集合,可以是字面量聯合類型 ('a' | 'b')、stringnumber 等。
  • 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

使用 hasOwnPropertykey 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)) 來過濾。

最佳實踐小結

  1. 明確宣告鍵的範圍:若鍵集合固定,使用聯合型別避免拼寫錯誤。
  2. 安全存取:總是檢查 undefined,或使用 ?? 0 作為預設值。
  3. 避免 any:保持值的型別具體,提升編譯期檢查的效益。
  4. 使用泛型抽象:在函式或類別中接受 Record<K, number>,讓 API 更彈性且安全。
  5. 寫測試:對於合併或累加的邏輯,加入單元測試確保不會因為鍵遺漏而產生錯誤。

實際應用場景

  1. 前端表單驗證計數

    • 針對使用者輸入的欄位名稱統計錯誤次數,Record<string, number> 可直接映射欄位 → 錯誤次數,方便在 UI 上顯示紅點或提示訊息。
  2. API 回傳字典資料

    • 某些 RESTful API 會回傳類似 { "USD": 1.0, "EUR": 0.85, "JPY": 110.5 } 的匯率表,使用 Record<string, number> 定義回傳型別,讓呼叫端在編譯時即知道每個幣別的值是數字。
  3. 遊戲分數排行榜

    • 玩家名稱(字串)對應分數(數字),使用 Record<string, number> 可以快速更新、查詢、排序,且在多人同時寫入時仍保持型別安全。
  4. 日誌統計與分析

    • 伺服器端收集不同類別的請求次數(例如 "GET": 1200, "POST": 300),Record<string, number> 能作為中間資料結構,之後再轉成圖表或報表。
  5. 設定檔與功能開關

    • 某些功能的權重或啟用次數以字串鍵儲存,使用 Record<string, number> 可以在 TypeScript 中直接驗證設定檔的結構,避免 JSON 錯誤導致程式崩潰。

總結

Record<string, number>TypeScript 中處理「字串鍵 → 數字值」映射的核心工具。透過本篇文章,我們了解了:

  • Record 的基本語法與意圖。
  • 為何在統計、合併、API 回傳等情境中選擇 Record<string, number>
  • 五個實務範例,從建立、累加、合併到型別守衛的完整流程。
  • 常見陷阱(undefined、鍵過寬、any)與對應的 最佳實踐
  • 真實的開發場景,說明此型別如何提升程式安全性與可維護性。

掌握這些概念後,你將能在 陣列與集合 的課程中,靈活運用 Record 來管理各式字典資料,寫出更可靠易讀易維護的 TypeScript 程式碼。祝你在實務開發中玩得開心、寫得順手! 🚀