TypeScript 基本型別 – Tuple(元組)
簡介
在 JavaScript 中,我們常用陣列 (Array) 來儲存一系列資料,但陣列的每個元素型別都會被視為相同。當開發大型應用程式時,需要同時描述多個不同型別的資料,這時就會遇到型別不夠精確、編譯器無法提供正確的型別檢查等問題。
TypeScript 為了解決這類需求,提供了 Tuple(元組) 這個概念。Tuple 允許我們在一個固定長度的結構裡,明確指定每個位置的型別,從而在編譯階段即捕捉到錯誤、提升程式可讀性與可維護性。
本篇文章將從基本概念出發,帶你一步步掌握 Tuple 的使用方式、常見陷阱與最佳實踐,並以實務案例說明它在真實專案中的價值。
核心概念
1. Tuple 是什麼?
Tuple 本質上是一種 固定長度且每個元素型別皆可自行定義 的陣列。語法上,我們使用方括號 [] 內部寫入型別列表來描述:
let point: [number, number]; // 2 維座標 (x, y)
此宣告表示 point 必須是一個長度為 2 的陣列,第一個元素必須是 number,第二個元素也必須是 number。若嘗試賦值為其他型別或長度,編譯器會直接報錯。
2. Tuple 與 Array 的差異
| 特性 | Array (T[]) |
Tuple ([T1, T2, ...]) |
|---|---|---|
| 長度 | 可變(任意長度) | 固定(宣告時即決定) |
| 每項型別 | 同質(全部同一型別) | 異質(每項可不同) |
| 型別推斷 | 只知道元素型別 T |
完整知道每個位置的型別 |
| 常見用途 | 大量同類資料 | 需要「多樣」且「有序」的資料組合 |
3. 基本語法與型別推斷
3.1 宣告與初始化
// 直接宣告並賦值,型別會自動推斷
let user: [string, number] = ["Alice", 30];
// 只宣告不賦值,必須明確指定型別
let response: [boolean, string];
response = [true, "OK"]; // 正確
// response = [false]; // ❌ 編譯錯誤:缺少第二個元素
3.2 讀取與寫入
let coords: [number, number, number] = [10, 20, 30];
// 讀取
let x = coords[0]; // x 為 number
let y = coords[1]; // y 為 number
// 寫入(只能寫入符合型別的值)
coords[2] = 40; // 正確
// coords[1] = "50"; // ❌ 編譯錯誤:string 不是 number
3.3 使用 push、pop 等陣列方法
Tuple 仍然是陣列的子類型,可以呼叫大部分陣列方法,但要注意 長度限制:
let pair: [string, number] = ["Bob", 25];
pair.push(100); // ✅ 仍允許,會把元素視為 number | string
pair.pop(); // ✅ 仍允許
// 此時 pair 的型別變為 (string | number)[]
// 為了保證 Tuple 的固定結構,建議盡量避免使用會改變長度的方法
小技巧:若想在不改變 Tuple 結構的情況下取得最後一個元素,可使用解構賦值或
at(-1)(ES2022):
let [, age] = pair; // 解構取得第二個元素
let last = pair.at(-1); // 取得最後一個元素
4. 可選元素與 Rest 元素
4.1 可選元素 (?)
// 第三個元素是可選的,若不存在則型別為 undefined
let rgb: [number, number, number?] = [255, 0];
rgb.push(128); // 現在變為 [255, 0, 128]
4.2 Rest 元素 (...)
Rest 元素允許 Tuple 的最後一個位置接受 任意長度的陣列,但所有元素必須符合指定的型別:
// 前兩個元素固定,後面可以接任意數量的 string
let message: [string, number, ...string[]] = ["Error", 404, "Not Found", "Please check URL"];
5. Tuple 的型別別名與介面
為了讓程式碼更具可讀性,常會把 Tuple 抽成型別別名或介面:
type Point2D = [x: number, y: number];
type HttpResponse = [status: number, body: string, headers?: Record<string, string>];
let p: Point2D = [5, 10];
let res: HttpResponse = [200, "Success"];
使用 命名參數 (x: number) 只是一種註解,不會影響編譯結果,但能提升可讀性。
程式碼範例
範例 1:簡易座標系統
type Point3D = [x: number, y: number, z: number];
function distance([x1, y1, z1]: Point3D, [x2, y2, z2]: Point3D): number {
const dx = x2 - x1;
const dy = y2 - y1;
const dz = z2 - z1;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
const a: Point3D = [0, 0, 0];
const b: Point3D = [3, 4, 12];
console.log(distance(a, b)); // 13
說明:透過解構直接取得座標值,程式碼更簡潔且型別安全。
範例 2:API 回傳值的型別定義
type ApiResult = [ok: boolean, data: any, error?: string];
function fetchUser(id: number): ApiResult {
if (id === 1) {
return [true, { name: "Alice", age: 30 }];
}
return [false, null, "User not found"];
}
const [ok, user, err] = fetchUser(2);
if (!ok) {
console.error(err); // "User not found"
} else {
console.log(user.name);
}
說明:使用 Tuple 讓回傳值的結構一目了然,呼叫端只需要解構即可取得每個欄位。
範例 3:可變長度的參數列表(Rest Tuple)
function logMessage(level: "info" | "warn" | "error", ...args: [string, ...any[]]): void {
const [msg, ...rest] = args;
console[level](`Message: ${msg}`, ...rest);
}
// 用法
logMessage("info", "Server started", { port: 3000 });
logMessage("error", "Connection failed", "timeout", 5000);
說明:...args 以 Tuple 定義,可保證第一個參數一定是 string,其後可接受任意數量的任意型別。
範例 4:結合 as const 產生字面量 Tuple
const STATUS = ["idle", "loading", "success", "error"] as const;
// STATUS 的型別為 readonly ["idle", "loading", "success", "error"]
type Status = typeof STATUS[number]; // "idle" | "loading" | "success" | "error"
function setStatus(s: Status) {
console.log(`Current status: ${s}`);
}
setStatus("loading"); // 正確
// setStatus("unknown"); // ❌ 編譯錯誤
說明:as const 讓陣列變成 只讀 Tuple,進而產生字面量聯合型別,常用於狀態機或列舉的替代方案。
範例 5:使用 Tuple 進行函式多返回值
function divide(dividend: number, divisor: number): [quotient: number, remainder: number] {
const q = Math.floor(dividend / divisor);
const r = dividend % divisor;
return [q, r];
}
const [q, r] = divide(10, 3);
console.log(`商 = ${q}, 餘數 = ${r}`); // 商 = 3, 餘數 = 1
說明:傳統上會回傳物件或陣列,使用 Tuple 可在型別層面保證返回值的順序與型別,讓呼叫端更清晰。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方式 / 最佳實踐 |
|---|---|---|
使用 push / pop 改變長度 |
會導致 Tuple 的型別變為聯合型別,失去固定結構的好處。 | 盡量避免,若真的需要,可在操作後使用 as const 重新斷言為原始 Tuple。 |
| 忘記標註可選元素 | 呼叫端在解構時可能得到 undefined,卻未處理。 |
明確在宣告時加上 ?,並在使用前檢查 undefined。 |
| Rest 元素混用不當 | Rest 元素只能放在最後,且只能有一個。 | 依需求設計結構,必要時拆成多層 Tuple 或使用介面。 |
| 過度使用 Tuple | 若結構過於複雜或層級太深,閱讀成本會上升。 | 當資料結構超過 3~4 個欄位時,考慮改用 介面 (interface) 或 型別別名 (type) 搭配物件。 |
| 未利用命名參數 | 只寫 [string, number] 雖正確,但閱讀者不易了解每個位置的意義。 |
使用 type Point = [x: number, y: number]; 為每個元素加上說明。 |
最佳實踐總結:
- 固定長度、型別明確時優先使用 Tuple。
- 避免改變長度的操作(
push、splice等),若必須,請在操作後重新斷言。 - 使用
as const產生只讀 Tuple,搭配typeof產生字面量型別,提升安全性。 - 為每個元素加上命名(
x: number),讓程式碼自說自明。 - 在公共 API 中,將 Tuple 抽成型別別名或介面,避免重複定義。
實際應用場景
資料庫查詢結果
多欄位的查詢(例如id,name,createdAt)可以用 Tuple 表示,確保每筆資料的欄位順序不會錯位。狀態機與列舉
使用as const結合 Tuple 定義有限集合,如 UI 的四種狀態 (idle,loading,success,error)。函式多返回值
例如divide、parseDate、fetchUser等需要同時返回多個相關值的函式,使用 Tuple 能保持返回值的順序與型別。事件系統
某些事件會傳遞固定型別的參數(例如mousemove會傳x、y),可以用 Tuple 定義事件資料結構,讓事件處理器的型別更精確。RESTful API 請求參數
某些 API 需要「路徑參數 + 查詢參數」的組合,使用 Tuple 可以一次描述所有必填與可選參數,減少錯誤。
總結
Tuple 是 TypeScript 為了彌補 JavaScript 陣列「同質」限制而設計的 強大型別工具。它允許我們在編譯階段就能捕捉到「位置型別不符」的錯誤,提升程式的可靠性與可讀性。
- 核心概念:固定長度、每個位置可自行指定型別,支援可選與 Rest 元素。
- 實作要點:宣告時使用方括號列出型別、適度使用
as const、避免改變長度的陣列方法。 - 最佳實踐:加上元素命名、抽成型別別名、在公共介面中統一管理。
掌握 Tuple 後,你將能更精確地描述資料結構,寫出更安全、更易維護的 TypeScript 程式碼。快把它運用在你的專案中,從函式的多返回值到 API 的回傳格式,讓型別成為你開發的最佳防線吧!