TypeScript – 物件與介面(Objects & Interfaces)
主題:Index Signature(索引簽名)
簡介
在日常開發中,我們常會碰到「屬性名稱在編譯時才知道」的情況,例如從 API 取得的設定檔、使用者自訂的字典或是動態產生的表格欄位。傳統的介面(interface)必須先寫死每一個屬性名稱,若屬性是未知的,就會失去型別安全的好處。
Index Signature(索引簽名)正是為了這類需求而設計的語法,它允許我們在介面或型別中描述「任意屬名」的結構,同時仍能保留 TypeScript 的靜態檢查與 IntelliSense。掌握索引簽名,不僅能寫出更彈性的程式碼,還能避免因「any」而失去型別保護的風險。
核心概念
1. 基本語法
interface StringMap {
[key: string]: string; // 任意字串鍵,其值必須是 string
}
- 方括號
[]內的 key 代表「索引類型」;左側可以是string、number或symbol(較少使用)。 - 冒號右側則是「對應的值類型」。
- 只要物件符合這個結構,無論它有多少個屬性,都會被視為
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' |
最佳實踐
- 先寫具體鍵:如果大部分鍵是已知的,就先在介面中列出,最後再加上寬鬆的索引簽名作為備援。
- 使用
Record:當鍵集合固定且值型別相同時,Record能讓程式碼更具可讀性。 - 加上
readonly:對於不會變動的設定檔或常數字典,務必使用只讀索引簽名,防止意外變更。 - 配合泛型:若要在函式中接受任意鍵的物件,可使用泛型
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_ENV 與 PORT 的必填檢查。
總結
- Index Signature 讓我們在 TypeScript 中描述「鍵名未知、值型別已知」的物件結構,解決了動態資料的型別安全問題。
- 基本語法是
[key: string]: Type,支援number、symbol,也可以與已知屬性混合使用。 - 加上
readonly、Record、或泛型,可提升可讀性、維護性與防止誤寫。 - 常見的坑包括已知屬性與索引簽名型別不匹配、過度使用
any、以及number與string索引的衝突。 - 在表單、國際化、API 回傳、設定檔等實務情境中,索引簽名都是不可或缺的工具。
掌握索引簽名後,你將能更靈活地設計資料結構,同時保有 TypeScript 強大的型別檢查與開發體驗。祝你在 TypeScript 的旅程中寫出更安全、更易維護的程式碼!