本文 AI 產出,尚未審核

TypeScript – 物件與介面(Objects & Interfaces)

主題:Index Signature(索引簽名)


簡介

在日常開發中,我們常會碰到「屬性名稱在編譯時才知道」的情況,例如從 API 取得的設定檔、使用者自訂的字典或是動態產生的表格欄位。傳統的介面(interface)必須先寫死每一個屬性名稱,若屬性是未知的,就會失去型別安全的好處。

Index Signature(索引簽名)正是為了這類需求而設計的語法,它允許我們在介面或型別中描述「任意屬名」的結構,同時仍能保留 TypeScript 的靜態檢查與 IntelliSense。掌握索引簽名,不僅能寫出更彈性的程式碼,還能避免因「any」而失去型別保護的風險。


核心概念

1. 基本語法

interface StringMap {
  [key: string]: string;   // 任意字串鍵,其值必須是 string
}
  • 方括號 [] 內的 key 代表「索引類型」;左側可以是 stringnumbersymbol(較少使用)。
  • 冒號右側則是「對應的值類型」。
  • 只要物件符合這個結構,無論它有多少個屬性,都會被視為 StringMap

範例 1:簡單的字典

const colors: StringMap = {
  red: "#ff0000",
  green: "#00ff00",
  blue: "#0000ff",
};

若把值寫成非 string,編譯器會立刻報錯:

// const wrong: StringMap = { red: 123 }; // ❌ Type 'number' is not assignable to type 'string'

2. 同時支援已知屬性與索引簽名

有時候我們既想定義幾個固定屬性,又要允許額外的動態屬性。這時可以把已知屬性寫在介面裡,然後再加上索引簽名:

interface Person {
  name: string;                // 必須有 name
  age: number;                 // 必須有 age
  [key: string]: string | number; // 其他屬性可以是 string 或 number
}

範例 2:擴充的使用者資料

const user: Person = {
  name: "Alice",
  age: 28,
  hobby: "photography",   // string, 符合索引簽名
  score: 95,              // number, 也符合
};

注意:已知屬性的型別必須是索引簽名允許的子集合,否則會產生衝突。


3. number 索引與 string 索引的關係

在 JavaScript 中,陣列的索引實際上是字串("0""1"…),所以 number 索引會自動映射到 string 索引。若同時宣告兩種索引,number 索引的值型別必須能夠賦值給 string 索引的值型別。

interface NumberKeyed {
  [key: number]: string; // number 索引
  [key: string]: string | undefined; // string 索引
}

範例 3:類似陣列的映射表

const scores: NumberKeyed = {
  0: "A",
  1: "B",
  // 也可以用字串鍵
  "2": "C",
};

4. 只讀索引簽名

如果資料不允許被修改,可以在索引簽名前加上 readonly

interface ReadonlyMap {
  readonly [key: string]: number;
}

範例 4:常數字典

const constants: ReadonlyMap = {
  MAX_USERS: 1000,
  TIMEOUT_MS: 3000,
};

// constants.MAX_USERS = 2000; // ❌ Cannot assign to 'MAX_USERS' because it is a read-only property.

5. 使用 Record 內建工具類型

TypeScript 提供了 Record<Keys, Type>,它本質上是 索引簽名的語法糖

type StatusMap = Record<'success' | 'error' | 'pending', number>;

等同於:

interface StatusMap {
  [key in 'success' | 'error' | 'pending']: number;
}

範例 5:狀態代碼表

const httpStatus: StatusMap = {
  success: 200,
  error: 500,
  pending: 102,
};

使用 Record 可以在 鍵集合 已知但值型別相同的情況下,寫得更簡潔。


常見陷阱與最佳實踐

陷阱 說明 解法
索引簽名與已知屬性型別不一致 已知屬性的型別必須是索引簽名的子集合,否則會產生「類型不相容」錯誤。 在設計介面時,先確定索引簽名的寬鬆度,再收斂已知屬性。
過度使用 any 為了迴避型別錯誤,直接把索引簽名寫成 [key: string]: any,會失去型別檢查的好處。 盡量使用具體的聯合型別或泛型,保留 IntelliSense。
number 索引與 string 索引衝突 若同時宣告兩者,number 的值型別必須能指派給 string 索引。 若不需要兩者同時存在,僅保留 string 索引即可;或使用 as const 斷言固定鍵。
忘記 readonly 動態字典有時只作為參考資料,卻被意外改寫。 在介面或 Record 前加上 readonly,讓編譯器保護資料不變。
鍵名過於寬鬆 [key: string]: number 允許任意字串鍵,可能會把打錯的屬性名稱也算進去。 可以結合 字面量聯合(`'width'

最佳實踐

  1. 先寫具體鍵:如果大部分鍵是已知的,就先在介面中列出,最後再加上寬鬆的索引簽名作為備援。
  2. 使用 Record:當鍵集合固定且值型別相同時,Record 能讓程式碼更具可讀性。
  3. 加上 readonly:對於不會變動的設定檔或常數字典,務必使用只讀索引簽名,防止意外變更。
  4. 配合泛型:若要在函式中接受任意鍵的物件,可使用泛型 T extends Record<string, any>,同時保留呼叫端的型別推斷。

實際應用場景

1. 前端表單動態欄位

interface FormValues {
  [field: string]: string | number | boolean;
}

function submit(form: FormValues) {
  // 依欄位名稱動態處理
  Object.entries(form).forEach(([key, value]) => {
    console.log(`欄位 ${key}: ${value}`);
  });
}

使用索引簽名可以讓表單欄位隨需求增減,而不必每次都修改介面。

2. 多語系資源檔

type Locale = 'en' | 'zh-TW' | 'ja';

interface Translations {
  [key: string]: string; // 任意字串鍵為翻譯文字
}

const i18n: Record<Locale, Translations> = {
  en: { welcome: "Welcome", logout: "Logout" },
  'zh-TW': { welcome: "歡迎", logout: "登出" },
  ja: { welcome: "ようこそ", logout: "ログアウト" },
};

Record 搭配索引簽名,讓語系結構一目了然且型別安全。

3. API 回傳的彈性 JSON

interface ApiResponse<T = any> {
  success: boolean;
  data: T;
  // 其他不確定的屬性
  [extra: string]: any;
}

即使 API 在不同版本中新增欄位,舊有程式碼仍能正常編譯,只要不依賴未定義的屬性。

4. 設定檔與環境變數

interface EnvConfig {
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: number;
  [key: string]: string | number; // 允許額外的自訂變數
}

const config: EnvConfig = {
  NODE_ENV: process.env.NODE_ENV ?? 'development',
  PORT: Number(process.env.PORT) ?? 3000,
  API_KEY: process.env.API_KEY ?? '',
};

透過索引簽名,開發者可以自由加入自訂環境變數,同時仍受到 NODE_ENVPORT 的必填檢查。


總結

  • Index Signature 讓我們在 TypeScript 中描述「鍵名未知、值型別已知」的物件結構,解決了動態資料的型別安全問題。
  • 基本語法是 [key: string]: Type,支援 numbersymbol,也可以與已知屬性混合使用。
  • 加上 readonlyRecord、或泛型,可提升可讀性、維護性與防止誤寫。
  • 常見的坑包括已知屬性與索引簽名型別不匹配、過度使用 any、以及 numberstring 索引的衝突。
  • 在表單、國際化、API 回傳、設定檔等實務情境中,索引簽名都是不可或缺的工具。

掌握索引簽名後,你將能更靈活地設計資料結構,同時保有 TypeScript 強大的型別檢查與開發體驗。祝你在 TypeScript 的旅程中寫出更安全、更易維護的程式碼!