本文 AI 產出,尚未審核

TypeScript 陣列泛型(Arrays & Collections)

簡介

在 JavaScript 中,陣列是最常見的資料結構;而在 TypeScript 加入型別系統之後,陣列的型別安全性 成為提升程式品質的關鍵。透過 陣列泛型(Array<T>),我們可以在編譯階段即捕捉到錯誤、避免不必要的執行時例外,讓程式更易維護、也更具可讀性。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握陣列泛型的用法,並提供實務上常見的應用情境。即使你是剛接觸 TypeScript 的新手,只要跟著步驟走,也能輕鬆寫出型別安全的陣列程式


核心概念

1. 為什麼要使用陣列泛型?

在純 JavaScript 中,陣列的元素型別是「任意」的,這會導致:

const list = [1, "two", true]; // 沒有編譯錯誤,但執行時可能出錯

使用 Array<T>(或簡寫 T[])可以把陣列限定在單一型別 T,讓編譯器在你把不相容的值塞進去時直接報錯。

const numbers: number[] = [1, 2, 3]; // 正確
// const wrong: number[] = [1, "two"]; // 編譯錯誤:類型 'string' 不能分配給類型 'number'

2. 基本語法

語法 說明
Array<T> 泛型介面,等同於 T[]
T[] 陣列型別的簡寫形式
readonly T[] 只讀陣列,內容不可變更
`Array<T undefined>`

小技巧:在需要 只讀 的情況下,使用 readonly 可防止意外修改,提升程式安全性。

3. 泛型與函式結合

當函式接受或回傳陣列時,使用泛型可以讓函式保持彈性,同時保有型別資訊。

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
const f1 = firstElement([10, 20, 30]); // f1 為 number
const f2 = firstElement(["a", "b"]);   // f2 為 string

4. 多維陣列

多維陣列同樣可以使用泛型描述:

const matrix: number[][] = [
  [1, 2],
  [3, 4],
];

如果想要更嚴格的維度(例如固定 2×2),可以利用 tuple 搭配泛型:

type Row = [number, number];
type Matrix2x2 = [Row, Row];
const fixedMatrix: Matrix2x2 = [
  [1, 2],
  [3, 4],
];

5. 內建泛型方法

許多陣列方法本身就帶有泛型,例如 map, filter, reduce。了解它們的型別推斷有助於寫出更安全的程式碼。

const nums = [1, 2, 3];
const doubled = nums.map(n => n * 2); // doubled 為 number[]

若使用自訂的回呼函式,仍可保留型別:

interface User {
  id: number;
  name: string;
}
const users: User[] = [{ id: 1, name: "Alice" }];

const names = users.map<User, string>(u => u.name); // names 為 string[]

程式碼範例

範例 1:基礎陣列泛型與型別推斷

// 定義一個只接受字串的陣列
const fruits: string[] = ["apple", "banana", "cherry"];

// 嘗試加入錯誤型別會在編譯時失敗
// fruits.push(123); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.

// 使用泛型函式取得最後一個元素
function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}
const lastFruit = last(fruits); // lastFruit 的型別是 string | undefined

範例 2:只讀陣列與淺層/深層不可變

// 只讀陣列:外部無法直接改變內容
const readonlyNums: readonly number[] = [10, 20, 30];
// readonlyNums[0] = 99; // Error: Index signature in type 'readonly number[]' only permits reading

// 若要深層不可變(陣列內部的物件也不可變),可使用 Readonly<T>
interface Point {
  x: number;
  y: number;
}
const points: ReadonlyArray<Readonly<Point>> = [
  { x: 0, y: 0 },
  { x: 10, y: 10 },
];
// points[0].x = 5; // Error: Cannot assign to 'x' because it is a read‑only property.

範例 3:泛型與條件型別結合 – 过滤 null/undefined

// 移除陣列中的 null 與 undefined
function compact<T>(arr: (T | null | undefined)[]): T[] {
  return arr.filter((v): v is T => v != null);
}

const mixed = [1, null, 2, undefined, 3];
const clean = compact(mixed); // clean 為 number[],值為 [1,2,3]

範例 4:自訂資料結構 – 堆疊 (Stack)

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  // 只讀快照
  toArray(): readonly T[] {
    return this.items;
  }
}

// 使用 Stack
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
const top = numberStack.pop(); // top 為 number | undefined,值為 20

範例 5:利用泛型實作「分組」函式

function groupBy<T, K extends keyof any>(
  arr: T[],
  keyGetter: (item: T) => K
): Map<K, T[]> {
  const map = new Map<K, T[]>();
  arr.forEach(item => {
    const key = keyGetter(item);
    const collection = map.get(key);
    if (collection) {
      collection.push(item);
    } else {
      map.set(key, [item]);
    }
  });
  return map;
}

// 範例:依照使用者年齡分組
interface Person {
  name: string;
  age: number;
}
const people: Person[] = [
  { name: "Amy", age: 20 },
  { name: "Bob", age: 25 },
  { name: "Cathy", age: 20 },
];
const byAge = groupBy(people, p => p.age);
// Map(2) { 20 => [{...}, {...}], 25 => [{...}] }

常見陷阱與最佳實踐

陷阱 描述 解決方式
混用 Array<T>T[] 雖然兩者等價,但在宣告 readonly 時語法不同,容易寫錯 盡量統一使用 T[](可讀寫)或 readonly T[](只讀),必要時才用 Array<T>
忘記處理 undefined Array<T> 只保證元素是 T,但 arr[0] 仍可能是 undefined(空陣列) 在存取前檢查長度或使用 可選鏈arr[0]?.prop
深層可變性 readonly T[] 只能防止改變陣列結構,內部物件仍可被修改 使用 Readonly<T>深層凍結Object.freeze
錯誤的泛型推斷 某些方法(如 reduce)若回呼未明確指定回傳型別,會退化成 any 明確寫出回呼的型別參數,或使用變數暫存結果再宣告型別
陣列與 tuple 混用 將 tuple 賦值給 T[] 會失去固定長度的保證 若需要固定長度,使用 readonly [T, U] 而非 T[]

最佳實踐

  1. 預設使用 readonly:對於不需要變更的資料(如 API 回傳的列表),直接宣告為 readonly T[],可在編譯階段捕捉不小心的寫入。
  2. 盡量讓函式保持純粹:接受 T[],回傳新陣列,而不是直接在原陣列上 mutate,這樣更符合函式式編程思維。
  3. 利用條件型別與型別守衛:在需要過濾或轉型時,使用 v is T 的型別守衛,可讓編譯器正確推斷結果型別。
  4. 在公共 API 中使用泛型:若你的函式庫要支援多種資料型別,使用泛型而非 any,可提升使用者的開發體驗。

實際應用場景

  1. 前端資料表格

    • 從後端取得 User[],使用 readonly User[] 直接作為表格的資料來源,避免 UI 誤寫入。
    • 透過 groupByfilter 等泛型函式,快速產生分組或搜尋結果,同時保有型別安全。
  2. Redux / NgRx 狀態管理

    • Store 中的陣列狀態通常以 readonly 形式保存,配合 immerimmutable.js,確保 reducer 不會直接 mutate。
  3. 資料驗證與清理

    • 使用 compact<T> 之類的泛型工具,將前端表單送出的混雜陣列(含空值)清理成純粹的業務型別。
  4. 自訂集合類別

    • 如範例中的 Stack<T>Queue<T>BinaryTree<T>,皆可藉由泛型讓同一套資料結構支援多種型別,減少重複程式碼。
  5. API 客戶端 SDK

    • SDK 中的每個方法都會回傳 Promise<T[]>,使用者在呼叫後即可得到正確的型別提示,避免手動 as 轉型的危險。

總結

  • 陣列泛型是 TypeScript 讓 JavaScript 陣列變得「型別安全」的核心機制。
  • 透過 Array<T>T[](加上 readonly)可以明確限制陣列元素型別,並在編譯階段即捕捉錯誤。
  • 結合 泛型函式條件型別型別守衛,可以寫出彈性又安全的資料處理程式。
  • 常見的陷阱包括忘記處理 undefined、深層可變性以及錯誤的型別推斷;遵守 只讀、純函式 的最佳實踐,可大幅降低錯誤率。
  • 在實務開發中,從 UI 表格、狀態管理到自訂集合類別,陣列泛型都是提升程式品質、降低維護成本的關鍵工具。

掌握了這些概念與技巧,你就能在 TypeScript 專案中自信地使用陣列,寫出安全、可讀、易維護的程式碼。祝你開發順利!