本文 AI 產出,尚未審核
TypeScript 陣列展開運算子與型別
簡介
在日常開發中,陣列是最常使用的資料結構之一。它不僅負責儲存同類型的資料,還提供了許多便利的操作方法。隨著 ES6 引入 展開運算子(spread operator),我們可以用更直覺的語法來合併、複製或展開陣列。而在 TypeScript 中,展開運算子不只是語法糖,還會牽涉到型別推斷與型別安全的問題。
本單元將說明如何在 TypeScript 中正確使用陣列展開運算子,並結合型別系統避免常見錯誤,幫助初學者快速上手、讓中級開發者在大型專案中亦能安全、有效地運用。
核心概念
1. 展開運算子的基本語法
const a = [1, 2, 3];
const b = [4, 5];
const c = [...a, ...b]; // [1, 2, 3, 4, 5]
...會把 可迭代物件(如陣列、字串、Set)展開成單一元素,再放入新的陣列或函式參數中。- 在 TypeScript 中,展開後的結果型別會根據來源陣列的型別自動推斷。
2. 型別推斷與聯合型別
const nums: number[] = [1, 2];
const strs: string[] = ['a', 'b'];
const mixed = [...nums, ...strs]; // inferred as (string | number)[]
- 當展開的陣列型別不一致時,TypeScript 會產生 聯合型別 (
string | number)。 - 若期望更嚴格的型別,可使用 型別斷言或 泛型 來限制。
3. 使用泛型保留元素順序與型別
function merge<T, U>(a: T[], b: U[]): (T | U)[] {
return [...a, ...b];
}
const result = merge([1, 2], ['x', 'y']); // (number | string)[]
- 透過泛型
T、U,函式可以接受任何型別的陣列,並正確推斷回傳的聯合型別。 - 若想保留 元組(tuple)的型別資訊,需要使用 條件型別與 展開元組。
4. 展開元組(Tuple)與可變長度
type Pair = [string, number];
const p: Pair = ['id', 42];
const extended = [...p, true]; // inferred as (string | number | boolean)[]
- 展開元組會把每個元素的型別依序展開,最終型別會是所有元素的 聯合型別。
- 若要保持元組結構,可使用
as const或...搭配infer的技巧。
5. 只讀陣列(ReadonlyArray)與展開
const ro: ReadonlyArray<number> = [10, 20];
const copy = [...ro]; // copy 為 number[]
- 展開運算子會 解除只讀限制,產生可變的普通陣列。
- 如果希望保留只讀屬性,需在展開後再使用
as const或ReadonlyArray包裝。
程式碼範例
範例 1:簡易陣列合併與型別推斷
const fruits = ['apple', 'banana'];
const numbers = [1, 2, 3];
// 展開後會得到 (string | number)[]
const mixed = [...fruits, ...numbers];
mixed.forEach(item => {
// TypeScript 會提示 item 可能是 string 或 number
if (typeof item === 'string') {
console.log('水果:', item);
} else {
console.log('數字:', item);
}
});
重點:展開不同型別的陣列會自動產生聯合型別,使用時需做好型別判斷。
範例 2:使用泛型的 merge 函式
function merge<T, U>(a: T[], b: U[]): (T | U)[] {
return [...a, ...b];
}
const nums = [10, 20];
const bools = [true, false];
const merged = merge(nums, bools); // (number | boolean)[]
技巧:若希望回傳的型別保持原始陣列的順序,可改寫為
Array<T | U>,或使用條件型別取得更精細的型別。
範例 3:保留元組型別資訊
type Point = [number, number];
const p1: Point = [0, 0];
const p2: Point = [10, 20];
// 直接展開會失去元組資訊
const flat = [...p1, ...p2]; // (number)[]
/* 想要保留兩個元組的結構,可使用以下技巧 */
type Concat<T extends any[], U extends any[]> = [...T, ...U];
type TwoPoints = Concat<Point, Point>; // [number, number, number, number]
const points: TwoPoints = [...p1, ...p2];
說明:透過 條件型別 + 展開元組,可以在編譯期保留完整的長度與型別資訊。
範例 4:只讀陣列的安全展開
const readonlyNums: ReadonlyArray<number> = [5, 6, 7];
// 展開後得到可變陣列
const mutableCopy = [...readonlyNums];
mutableCopy.push(8); // OK
// 若想保持只讀,可再包裝一次
const stillReadonly: ReadonlyArray<number> = [...readonlyNums] as const;
最佳實踐:在需要傳遞只讀資料給外部函式時,避免直接展開成可變陣列,以免意外修改。
範例 5:函式參數的展開與型別限制
function sum(...values: number[]): number {
return values.reduce((a, b) => a + b, 0);
}
const arr = [1, 2, 3];
const total = sum(...arr); // 正常
// 若傳入非 number 陣列會在編譯期錯誤
const strArr = ['a', 'b'];
// const wrong = sum(...strArr); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
重點:使用展開運算子傳遞參數時,TypeScript 會檢查每個元素的型別,確保函式簽名相容。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 展開後失去只讀屬性 | ...readonlyArray 會產生可變陣列。 |
若需保持只讀,使用 as const 或重新包裝成 ReadonlyArray。 |
| 聯合型別過寬 | 合併不同型別陣列時,型別會變成寬鬆的聯合型別,導致後續操作需要額外型別保護。 | 使用 泛型 或 型別斷言 (as) 限制回傳型別,或在設計資料結構時盡量保持同質性。 |
| 展開元組失去長度資訊 | 直接展開會變成普通陣列,失去元組的固定長度檢查。 | 利用 條件型別 (Concat<T,U>) 或 as const 重新建立元組型別。 |
| 展開非可迭代物件 | 嘗試 ...nonIterable 會在編譯期或執行期拋錯。 |
確認變數實際為 Array、ReadonlyArray、Set、Map、string 等可迭代物件。 |
過度使用 any |
為了避免型別錯誤而直接使用 any,失去 TypeScript 的安全保障。 |
儘量使用 具體型別、泛型,或在必要時使用 unknown 再做型別縮小。 |
最佳實踐
- 保持同質性:在同一個陣列中盡量存放相同型別的元素,減少不必要的聯合型別。
- 使用
as const:對於不會變更的字面值陣列,使用as const讓 TypeScript 推斷為 只讀且具體的字面型別。 - 泛型函式:若需要處理多種陣列,寫成泛型函式能同時保留型別資訊與可重用性。
- 避免過度展開:在大型陣列或深層結構中頻繁展開會產生大量臨時陣列,影響效能。可考慮
Array.concat或 迭代器 替代。
實際應用場景
Redux / Zustand 狀態合併
const newTodos = [...state.todos, action.payload]; // TypeScript 會保證 newTodos 為 Todo[]展開運算子讓我們以 不可變 的方式新增元素,同時保留型別安全。
API 回傳分頁資料合併
const allItems = [...prevPage.items, ...nextPage.items]; // allItems 仍然是 Item[],方便後續渲染或過濾函式參數的可變長度
function logAll(...messages: string[]) { console.log(...messages); } const msgs = ['a', 'b', 'c']; logAll(...msgs);透過展開,我們可以把陣列直接傳入接受不定參數的函式。
測試資料的快速組裝
const baseUser = { id: 1, name: 'Alice' } as const; const extra = [{ role: 'admin' }, { active: true }]; const user = { ...baseUser, ...extra[0], ...extra[1] }; // user 的型別會是 { id: 1; name: "Alice"; role: "admin"; active: true }組合多個設定檔
const defaultConfig = { timeout: 3000, retries: 3 } as const; const envConfig = { timeout: 5000 } as const; const finalConfig = { ...defaultConfig, ...envConfig }; // finalConfig 的型別為 { timeout: number; retries: number }
總結
- 展開運算子是 ES6 提供的強大語法,讓陣列合併、複製、參數傳遞變得簡潔。
- 在 TypeScript 中,展開會同時觸發型別推斷,可能產生聯合型別、失去只讀屬性或元組長度資訊。
- 透過 泛型、條件型別、
as const等技巧,我們可以在保留型別安全的同時,享受展開運算子的便利。 - 識別常見陷阱(如過寬的聯合型別、只讀屬性遺失)並遵守最佳實踐,能讓程式碼更健全、更易維護。
掌握了這些概念與技巧後,你就能在日常開發、狀態管理、API 整合等各種情境下,安全且高效地使用陣列展開運算子,讓 TypeScript 的型別系統為你的程式碼保駕護航。祝你寫程式愉快!