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)後再處理。 |
最佳實踐總結:
- 明確宣告型別:盡量避免
any,使用number[][]、type Matrix = number[][]等。 - 使用
readonly或as const:保護不可變資料,減少副作用。 - 型別別名 + Tuple:提升可讀性與編譯安全,尤其在固定長度的矩陣中。
- 封裝共用邏輯:像
flatten、createNDArray這類工具函式,放在共用模組,讓團隊一致使用。 - 測試邊界條件:特別是「不規則」的多維陣列,寫單元測試防止
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 程式。祝開發順利! 🚀