本文 AI 產出,尚未審核

TypeScript – 陣列與集合(Arrays & Collections)

主題:多維陣列型別


簡介

在日常開發中,陣列是最常見的資料結構之一。當資料呈現 表格座標系階層樹 的形態時,我們往往需要 多維陣列(或稱二維、三維…)來儲存與操作。
在 TypeScript 中,雖然 JavaScript 本身只提供單層的 Array,但透過 型別系統,我們可以為任意深度的巢狀陣列建立清晰、可預測的型別,從而在編譯階段捕捉錯誤、提升開發效率。

本篇文章將帶你從 基本概念實作範例,到 常見陷阱最佳實踐,全方位掌握多維陣列型別的使用方式,讓你在專案中得心應手。


核心概念

1. 什麼是多維陣列?

多維陣列其實是 陣列的陣列(Array of Arrays),每一層都可以視為一個子陣列。
例如,二維陣列可視為「列」與「行」的矩陣;三維陣列則可視為「層」「列」「行」的立體資料。

在 TypeScript 中,我們可以用以下兩種方式描述:

// 直接寫出巢狀的 Array 型別
let matrix: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
];

// 使用泛型寫法
let cube: Array<Array<Array<string>>> = [
  [
    ["a", "b"], ["c", "d"]
  ],
];

兩者在編譯結果相同,選擇哪種寫法主要看 可讀性團隊慣例


2. 型別別名(type alias)讓多維陣列更易讀

當巢狀層級變深或元素型別較複雜時,直接寫 number[][][][] 會讓程式碼變得難以閱讀。此時,我們可以使用 型別別名

// 定義一個二維數字矩陣的別名
type Matrix = number[][];

// 定義三維字串立方體的別名
type Cube = string[][][];

// 使用別名
let scores: Matrix = [
  [10, 20],
  [30, 40],
];
let words: Cube = [
  [
    ["hello", "world"],
    ["foo", "bar"]
  ]
];

別名不僅提升可讀性,也方便在多個檔案間共用相同結構。


3. Tuple 與固定長度的多維陣列

有時候我們需要 固定長度固定型別順序 的多維陣列,這時可以結合 Tuple

// 2x3 的固定矩陣:2 列、每列 3 個 number
type FixedMatrix = [ [number, number, number], [number, number, number] ];

const fixed: FixedMatrix = [
  [1, 2, 3],
  [4, 5, 6],
];

// 若長度不符,編譯會報錯
// const wrong: FixedMatrix = [[1,2], [3,4,5]]; // ❌

使用 Tuple 可以在編譯階段保證「形狀」正確,對於座標、顏色矩陣等固定結構特別有用。


4. 只讀多維陣列(ReadonlyArray)

在某些情境下,我們希望 禁止修改 陣列內容,以避免意外的副作用。TypeScript 提供 readonly 關鍵字或 ReadonlyArray<T>

// 只讀二維陣列
type ReadonlyMatrix = ReadonlyArray<ReadonlyArray<number>>;

const immutable: ReadonlyMatrix = [
  [0, 1],
  [2, 3],
];

// immutable[0][0] = 9; // ❌ 編譯錯誤

只讀陣列在 函式參數API 回傳值 中非常常見,能保證資料的不可變性。


5. 泛型與遞迴型別:建立任意深度的多維陣列

如果想要寫一個 可接受任意維度 的函式(例如深度拷貝、展平),可以利用遞迴型別:

// 產生任意深度的陣列型別
type NDArray<T, D extends number> = D extends 0
  ? T
  : NDArray<T, Decrement[D]>[];

type Decrement = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; // 簡易遞減映射

// 範例:3 維的 number 陣列
type ThreeD = NDArray<number, 3>;

const threeDim: ThreeD = [
  [
    [1, 2],
    [3, 4],
  ],
  [
    [5, 6],
    [7, 8],
  ],
];

雖然寫起來稍微複雜,但一旦定義好,就能在整個專案中 統一管理 任意深度的陣列型別。


程式碼範例(實用示範)

以下提供 5 個常見情境 的完整範例,包含註解說明,讓你快速上手。

範例 1:二維數字矩陣的加總

/** 計算二維矩陣中所有元素的總和 */
function sumMatrix(matrix: number[][]): number {
  let total = 0;
  for (const row of matrix) {
    for (const cell of row) {
      total += cell;
    }
  }
  return total;
}

const mat = [
  [1, 2, 3],
  [4, 5, 6],
];
console.log(sumMatrix(mat)); // 21

重點number[][] 明確表示「每一列都是 number[]」,編譯器會保證 row 內只能是 number


範例 2:使用型別別名與只讀保護

type ReadonlyMatrix = ReadonlyArray<ReadonlyArray<number>>;

/** 取得指定座標的值,若超出範圍回傳 undefined */
function getValue(
  matrix: ReadonlyMatrix,
  row: number,
  col: number
): number | undefined {
  return matrix[row]?.[col];
}

const board: ReadonlyMatrix = [
  [0, 1, 2],
  [3, 4, 5],
] as const; // `as const` 讓陣列變成只讀

console.log(getValue(board, 1, 2)); // 5
// board[0][0] = 9; // ❌ 編譯錯誤,保護資料不被修改

技巧as const 能一次把所有巢層都變成只讀,適合靜態資料(例如棋盤、預設配置)。


範例 3:固定長度的三維座標陣列(Tuple)

// 每個座標點是 [x, y, z],整體是一個座標列表
type Point3D = [number, number, number];
type PointList = Point3D[];

const path: PointList = [
  [0, 0, 0],
  [10, 5, 2],
  [20, 15, 8],
];

// 計算兩點之間的歐式距離
function distance(a: Point3D, b: Point3D): number {
  const dx = b[0] - a[0];
  const dy = b[1] - a[1];
  const dz = b[2] - a[2];
  return Math.sqrt(dx * dx + dy * dy + dz * dz);
}

console.log(distance(path[0], path[1])); // 11.357...

說明:使用 Tuple 可保證每個座標恰好有 三個數值,避免不小心寫成 [x, y] 或多了其他屬性。


範例 4:遞迴型別實作任意維度的展平(flatten)

// 深層展平的型別(保留最終元素型別)
type Flatten<T> = T extends (infer U)[] ? Flatten<U> : T;

/** 任意深度陣列的展平函式 */
function flatten<T>(arr: any[]): Flatten<T>[] {
  const result: any[] = [];
  for (const item of arr) {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

// 測試
const nested = [[1, 2], [3, [4, 5]], 6];
const flat = flatten<number>(nested);
console.log(flat); // [1,2,3,4,5,6]

// `flat` 的型別被推斷為 `number[]`

關鍵Flatten<T> 透過條件型別遞迴抽取最底層元素型別,使得 flatten 的回傳型別正確保留 元素的原始型別


範例 5:使用泛型建立可設定維度的多維陣列工廠

/** 產生指定維度、預設值的多維陣列 */
function createNDArray<T>(dims: number[], init: T): any {
  if (dims.length === 0) return init;
  const [first, ...rest] = dims;
  const arr = [];
  for (let i = 0; i < first; i++) {
    arr[i] = createNDArray(rest, init);
  }
  return arr;
}

// 建立 3x2x4 的字串陣列,預設值為 '-'
const threeD = createNDArray<string>([3, 2, 4], '-');
console.log(threeD[0][1][2]); // '-'

實務:此工廠函式在 測試資料產生預設表格、或 多維緩衝區 時非常實用,只要提供維度與初始值即可。


常見陷阱與最佳實踐

陷阱 說明 解決方案
陣列深度不一致(jagged array) 兩列長度不同會導致迭代時出現 undefined 若需要固定形狀,使用 Tuple 或自行檢查長度;若允許不規則,使用 Array<Array<T>> 並在程式碼中做好 null/undefined 防護。
any 失去型別保護 把多維陣列寫成 any[][] 會讓編譯器無法檢查元素型別。 儘量使用 具體型別number[][]string[][][]),或透過 型別別名 抽象化。
只讀陣列仍可被深層修改 readonly T[] 只保護第一層,巢層仍是可變的。 使用 ReadonlyArray<ReadonlyArray<...>>as const 讓所有層級都變成只讀。
迭代時忘記 Array.isArray 直接存取 item[i] 可能在非陣列值上拋錯。 在遞迴或深層操作前,先檢查 Array.isArray(item)
維度過深造成效能問題 多層巢狀迴圈會增加 CPU 與記憶體開銷。 只在必要時使用深度結構;若大量運算,考慮 平坦化(flatten)後再處理。

最佳實踐總結

  1. 明確宣告型別:盡量避免 any,使用 number[][]type Matrix = number[][] 等。
  2. 使用 readonlyas const:保護不可變資料,減少副作用。
  3. 型別別名 + Tuple:提升可讀性與編譯安全,尤其在固定長度的矩陣中。
  4. 封裝共用邏輯:像 flattencreateNDArray 這類工具函式,放在共用模組,讓團隊一致使用。
  5. 測試邊界條件:特別是「不規則」的多維陣列,寫單元測試防止 undefined 或越界錯誤。

實際應用場景

場景 為何需要多維陣列 典型型別
棋盤遊戲(例如井字棋、圍棋) 2D 網格表示每格狀態 readonly number[][]
影像處理(像素矩陣) 每個像素有 R,G,B 三個分量,且排列成 2D 圖像 Uint8ClampedArray[][]number[][][]
3D 渲染(體素、立體模型) 需要 X、Y、Z 三軸座標的數值陣列 number[][][]
財務報表(年度 × 月度 × 科目) 多層次的彙總資料 Record<string, number[][]>
機器學習張量(Tensor) 任意維度的數值張量,用於矩陣運算 number[] 結合 外部套件(如 tfjs)或自訂 NDArray

範例:在一個簡易的 井字棋 程式中,我們可以這樣宣告棋盤:

type TicTacToeBoard = readonly ('X' | 'O' | null)[][];

const board: TicTacToeBoard = [
  [null, null, null],
  [null, null, null],
  [null, null, null],
] as const;

使用 只讀聯合型別,既保證了資料不可變,又限制了格子只能是 'X''O'null,減少錯誤。


總結

多維陣列在 TypeScript 中不僅是 資料結構,更是 型別安全 的最佳展示。透過:

  • 直接的巢狀 Array<T>泛型寫法
  • 型別別名 讓結構更易讀
  • Tuple 保障固定長度與順序
  • Readonly 確保不可變性
  • 遞迴型別 甚至可以描述任意深度的 NDArray

我們可以在開發過程中 提前捕捉錯誤提升程式碼可維護性,同時保持執行效能。熟悉上述概念與範例,配合最佳實踐與常見陷阱的防範,將讓你在處理表格、座標、影像或任何需要巢狀資料的情境時,得心應手、寫出高品質的 TypeScript 程式。祝開發順利! 🚀