TypeScript 資料型別 - 元組(Tuple) & 列舉(Enum)
TypeScript 在 array 與 object 之外,提供兩種「結構化常用型別」:元組(Tuple) 把一個陣列的「每個位置」鎖定型別與長度;列舉(Enum) 把一組命名常數收成一個型別。兩者都是面試常考的「你知不知道它的坑」題型。
元組(Tuple)
元組是固定長度 + 每個位置型別固定的陣列。一般 array 是「同一型別、長度任意」,元組是「位置即型別」。
// 一般 array:同型別、長度任意
let nums: number[] = [1, 2, 3];
// 元組:位置 0 必須 string、位置 1 必須 number,長度固定為 2
let user: [string, number] = ['Alice', 25];
user = [25, 'Alice']; // ❌ 型別順序錯
user = ['Alice']; // ❌ 長度不足
user = ['Alice', 25, true]; // ❌ 超出長度
Labeled Tuple(具名元組)
只是標籤,不影響型別,純粹增加可讀性(IDE 提示會顯示名稱)。
type Coord = [x: number, y: number];
type Range = [start: number, end: number];
const c: Coord = [10, 20];
標籤要嘛全標、要嘛全不標,不能混用:[x: number, number] 會編譯錯誤。
Optional element(可選元素)
用 ? 標記可選元素,只能放在尾端,且會放寬長度限制。
let point: [number, number, number?] = [10, 20]; // z 可省略
point = [10, 20, 30]; // ✅
// ❌ 可選元素不能在必填元素前面
type Bad = [a?: number, b: number]; // Error
Rest element(其餘元素)
用 ...T[] 表示「前面固定、後面不定長」,可放在任意位置。
// 第一個是 string,後面任意數量 number
type StringThenNumbers = [string, ...number[]];
const a: StringThenNumbers = ['sum', 1, 2, 3]; // ✅
// rest 也可放開頭或中間
type AtLeastOne = [...string[], number]; // 結尾必為 number
最經典的用途是替函式 arguments 或可變參數定型:
function concat<T extends unknown[]>(...args: [...T, string]): T {
// ...
return args.slice(0, -1) as T;
}
唯讀元組(readonly tuple)
加 readonly 後元素不可被重新賦值或 mutate,常配合 as const 使用。
const rgb: readonly [number, number, number] = [255, 0, 0];
rgb[0] = 0; // ❌ Cannot assign to '0' because it is read-only
rgb.push(1); // ❌ readonly tuple 沒有 push
// as const 產生的就是 readonly tuple
const point = [10, 20] as const; // type: readonly [10, 20]
常見用途與陷阱
// React useState 回傳值本身就是 tuple
const [count, setCount] = useState(0);
// type: [number, Dispatch<SetStateAction<number>>]
陷阱:push 能繞過長度限制非 readonly 元組仍是陣列,TS 不會擋 push:let t: [string, number] = ['a', 1]; t.push(2); // ⚠️ 不報錯,runtime 變成長度 3 console.log(t[2]); // 但用 index 2 存取會型別報錯真的要鎖死長度,用
readonly元組。
| 寫法 | 長度 | 可變 | 用途 |
|---|---|---|---|
[string, number] | 固定 2(但 push 可繞過) | ✅ | 一般成對資料 |
[string, number?] | 1 或 2 | ✅ | 尾端可省略 |
[string, ...number[]] | ≥ 1 | ✅ | 可變參數定型 |
readonly [string, number] | 固定 2(鎖死) | ❌ | 不可變的座標/常數 |
列舉(Enum)
enum 用於定義一組命名常數,提升可讀性,並把這組值收斂成一個型別。
數字列舉(Numeric Enum)
不給值時從 0 開始自動遞增;指定某個值後,後續成員從該值繼續遞增。
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
enum Code {
A = 5,
B, // 6(接續遞增)
C, // 7
}
數字列舉支援反向對映(reverse mapping):既能 名稱 → 值,也能 值 → 名稱。
Direction.Up; // 0
Direction[0]; // 'Up' ← 反向對映
原理是編譯後產生雙向 key:
// 編譯結果(節錄)
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));
字串列舉(String Enum)
每個成員都必須明確賦值(不會自動遞增)。
enum Status {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING',
}
字串列舉沒有反向對映:Status['ACTIVE'] 是 undefined。只有數字列舉才產生 值 → 名稱 的反向 key。
字串列舉通常比數字列舉更實用:debug log / 序列化時看到的是 'ACTIVE' 而不是難懂的 0。
別混用會踩雷enum Mixed { A, // 0 B = 'TWO', // 字串 C, // ❌ 前一個是字串,C 無法自動遞增 → 報錯 }
const enum
加 const 後,編譯時會把每個用到的地方直接內聯成字面值(inline),不產生對應的 JS 物件。
const enum HttpStatus {
OK = 200,
NotFound = 404,
}
const s = HttpStatus.OK;
// 編譯後 → const s = 200; (HttpStatus 物件整個消失)
| 一般 enum | const enum | |
|---|---|---|
| 編譯產物 | 產生 runtime 物件 | 內聯字面值,無物件 |
| 反向對映 | 數字 enum 支援 | ❌ 不支援(物件不存在) |
動態存取 E[key] | ✅ | ❌(無物件可查) |
| bundle 體積 | 較大 | 較小 |
const enum 的隔離編譯問題在 isolatedModules(Babel / esbuild / SWC 等單檔轉譯器)下,const enum 無法跨檔內聯,會出錯或行為不符。Vite / Next.js SWC 這類工具鏈要特別注意,官方也建議一般情況避免 const enum。
Enum vs Union Type vs as const
現代 TypeScript 中,很多人偏好用 union type 或 as const object 取代 enum。核心原因:enum 會產生額外的 runtime 程式碼(它不是純型別,是真實存在的 JS 物件),而 union / as const 是「型別層的東西」,零 runtime 成本、tree-shaking 友善。
// 1. Enum:有 runtime 物件
enum Color { Red = 'RED', Green = 'GREEN' }
// 2. Union type:純型別,零 runtime
type Color = 'RED' | 'GREEN';
// 3. as const object:常數物件 + 衍生 union(兩者兼得)
const Color = { Red: 'RED', Green: 'GREEN' } as const;
type Color = typeof Color[keyof typeof Color]; // 'RED' | 'GREEN'
as const 寫法兼具「可像 enum 一樣用 Color.Red 取值」與「衍生出 union 型別」,又不犧牲 runtime 體積,是目前社群的主流推薦。
| 需求 | 建議 |
|---|---|
| 只需要一組合法字串/數字、不需要 runtime 物件 | Union type ('a' | 'b') |
需要像 Color.Red 一樣取值,又要 union 型別、又在意體積 | as const object |
| 需要反向對映(值 → 名稱) | 數字 enum(唯一原生支援的) |
| 既有專案大量使用、團隊習慣 | 維持 enum 即可,不必硬改 |
面試一句話總結Tuple 解決「固定長度 + 每位置型別」;Enum 解決「命名常數集合」,但因為有 runtime 成本與 const enum 的隔離編譯雷,現代多用 as const / union 取代——除非你需要數字 enum 的反向對映。