本文 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[] |
最佳實踐
- 預設使用
readonly:對於不需要變更的資料(如 API 回傳的列表),直接宣告為readonly T[],可在編譯階段捕捉不小心的寫入。 - 盡量讓函式保持純粹:接受
T[],回傳新陣列,而不是直接在原陣列上 mutate,這樣更符合函式式編程思維。 - 利用條件型別與型別守衛:在需要過濾或轉型時,使用
v is T的型別守衛,可讓編譯器正確推斷結果型別。 - 在公共 API 中使用泛型:若你的函式庫要支援多種資料型別,使用泛型而非
any,可提升使用者的開發體驗。
實際應用場景
前端資料表格
- 從後端取得
User[],使用readonly User[]直接作為表格的資料來源,避免 UI 誤寫入。 - 透過
groupBy或filter等泛型函式,快速產生分組或搜尋結果,同時保有型別安全。
- 從後端取得
Redux / NgRx 狀態管理
- Store 中的陣列狀態通常以
readonly形式保存,配合 immer 或 immutable.js,確保 reducer 不會直接 mutate。
- Store 中的陣列狀態通常以
資料驗證與清理
- 使用
compact<T>之類的泛型工具,將前端表單送出的混雜陣列(含空值)清理成純粹的業務型別。
- 使用
自訂集合類別
- 如範例中的
Stack<T>、Queue<T>、BinaryTree<T>,皆可藉由泛型讓同一套資料結構支援多種型別,減少重複程式碼。
- 如範例中的
API 客戶端 SDK
- SDK 中的每個方法都會回傳
Promise<T[]>,使用者在呼叫後即可得到正確的型別提示,避免手動as轉型的危險。
- SDK 中的每個方法都會回傳
總結
- 陣列泛型是 TypeScript 讓 JavaScript 陣列變得「型別安全」的核心機制。
- 透過
Array<T>或T[](加上readonly)可以明確限制陣列元素型別,並在編譯階段即捕捉錯誤。 - 結合 泛型函式、條件型別與 型別守衛,可以寫出彈性又安全的資料處理程式。
- 常見的陷阱包括忘記處理
undefined、深層可變性以及錯誤的型別推斷;遵守 只讀、純函式 的最佳實踐,可大幅降低錯誤率。 - 在實務開發中,從 UI 表格、狀態管理到自訂集合類別,陣列泛型都是提升程式品質、降低維護成本的關鍵工具。
掌握了這些概念與技巧,你就能在 TypeScript 專案中自信地使用陣列,寫出安全、可讀、易維護的程式碼。祝你開發順利!