本文 AI 產出,尚未審核

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 使用 pushpop 等陣列方法

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]; 為每個元素加上說明。

最佳實踐總結

  1. 固定長度、型別明確時優先使用 Tuple。
  2. 避免改變長度的操作(pushsplice 等),若必須,請在操作後重新斷言。
  3. 使用 as const 產生只讀 Tuple,搭配 typeof 產生字面量型別,提升安全性。
  4. 為每個元素加上命名x: number),讓程式碼自說自明。
  5. 在公共 API 中,將 Tuple 抽成型別別名或介面,避免重複定義。

實際應用場景

  1. 資料庫查詢結果
    多欄位的查詢(例如 id, name, createdAt)可以用 Tuple 表示,確保每筆資料的欄位順序不會錯位。

  2. 狀態機與列舉
    使用 as const 結合 Tuple 定義有限集合,如 UI 的四種狀態 (idle, loading, success, error)。

  3. 函式多返回值
    例如 divideparseDatefetchUser 等需要同時返回多個相關值的函式,使用 Tuple 能保持返回值的順序與型別。

  4. 事件系統
    某些事件會傳遞固定型別的參數(例如 mousemove 會傳 xy),可以用 Tuple 定義事件資料結構,讓事件處理器的型別更精確。

  5. RESTful API 請求參數
    某些 API 需要「路徑參數 + 查詢參數」的組合,使用 Tuple 可以一次描述所有必填與可選參數,減少錯誤。


總結

Tuple 是 TypeScript 為了彌補 JavaScript 陣列「同質」限制而設計的 強大型別工具。它允許我們在編譯階段就能捕捉到「位置型別不符」的錯誤,提升程式的可靠性與可讀性。

  • 核心概念:固定長度、每個位置可自行指定型別,支援可選與 Rest 元素。
  • 實作要點:宣告時使用方括號列出型別、適度使用 as const、避免改變長度的陣列方法。
  • 最佳實踐:加上元素命名、抽成型別別名、在公共介面中統一管理。

掌握 Tuple 後,你將能更精確地描述資料結構,寫出更安全、更易維護的 TypeScript 程式碼。快把它運用在你的專案中,從函式的多返回值到 API 的回傳格式,讓型別成為你開發的最佳防線吧!