TypeScript 函式(Functions) – Rest 參數
簡介
在 JavaScript 與 TypeScript 中,函式是程式的核心組件。隨著需求的多樣化,我們常常需要 接受不定數量的參數,例如 console.log、Array.prototype.push 等內建函式皆支援此類呼叫方式。
TypeScript 提供的 Rest 參數(...args)不僅能收集任意數量的輸入,還能在編譯階段保留型別資訊,讓開發者在編寫彈性函式時仍能享有靜態檢查的好處。
本篇文章將逐步說明 Rest 參數的語法、型別推斷、常見陷阱以及實務應用,幫助 初學者到中階開發者 能夠在專案中安全、有效地使用它。
核心概念
1. 什麼是 Rest 參數?
Rest 參數使用展開語法 ... 放在函式參數的最後一個位置,表示「收集剩餘的所有參數」並以陣列的形式傳入函式內部。
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
...numbers會把呼叫時傳入的所有參數(不限個數)聚合成number[]。- 必須是 最後一個參數,因為之後的參數無法被「收集」進陣列。
2. 型別註記的寫法
在 TypeScript 中,我們可以為 Rest 參數指定 陣列型別,或直接使用 元組型別(Tuple)來限定每個元素的型別與長度。
// 只接受字串
function join(...parts: string[]): string {
return parts.join(' ');
}
// 使用元組限制前兩個參數的型別,剩餘的任意
function format(template: string, ...values: [number, boolean, ...any[]]): string {
// template: "ID: %d, Active: %b, ..."
// values[0] 必須是 number,values[1] 必須是 boolean
// 之後可以再放任意型別
return '';
}
3. 與 Spread (展開) 的關係
Rest 參數與 Spread 語法長得很像,實際上兩者是相反的概念:
- Rest:在函式定義時,把多個參數「收集」成一個陣列。
- Spread:在呼叫函式或建立陣列時,把陣列「展開」成多個獨立的值。
function log(...messages: string[]) {
console.log(...messages); // 這裡使用 Spread 把陣列展開
}
log('Hello', 'World'); // Rest 收集兩個字串
4. 結合泛型的 Rest 參數
泛型讓 Rest 參數更具彈性,能根據呼叫時的實際參數自動推導型別。
function tuple<T extends unknown[]>(...args: T): T {
return args;
}
// 推導結果:tuple(1, 'a', true) => [number, string, boolean]
const result = tuple(1, 'a', true);
5. 參數重載與 Rest
在需要支援多種參數組合時,函式重載 搭配 Rest 參數非常好用。
function overload(a: string, b: number): string;
function overload(...args: [string, number] | [number, string]): string;
function overload(...args: any[]): string {
if (typeof args[0] === 'string') {
return `String first: ${args[0]}, Number second: ${args[1]}`;
}
return `Number first: ${args[0]}, String second: ${args[1]}`;
}
程式碼範例
以下提供 5 個實用範例,說明 Rest 參數在不同情境下的寫法與注意點。
範例 1️⃣ 基本求和函式
function sum(...values: number[]): number {
// 使用 Array.prototype.reduce 累加
return values.reduce((acc, cur) => acc + cur, 0);
}
// 呼叫方式
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum()); // 0(空陣列也安全)
重點:即使沒有傳入參數,
values仍是一個空陣列,reduce需要提供初始值0以避免錯誤。
範例 2️⃣ 文字拼接與分隔符
function join(separator: string, ...parts: string[]): string {
return parts.join(separator);
}
// 使用
console.log(join(', ', 'apple', 'banana', 'cherry')); // apple, banana, cherry
說明:第一個參數
separator必須寫在 Rest 參數之前,否則會被收集進parts陣列。
範例 3️⃣ 泛型 Rest 參數 – 產生 Tuple
function makeTuple<T extends unknown[]>(...elements: T): T {
return elements;
}
// 推斷結果
const t1 = makeTuple(42, 'answer', true); // [number, string, boolean]
type T1 = typeof t1; // -> [number, string, boolean]
技巧:利用
T extends unknown[]可以保留每個元素的具體型別,讓回傳值成為精確的元組。
範例 4️⃣ 結合 Spread 實作「參數轉發」
function logAll(...msgs: string[]) {
// 把陣列展開傳給 console.log
console.log(...msgs);
}
// 先收集,再轉發
function wrapper(...args: string[]) {
// 可以在此插入前置處理
logAll(...args);
}
wrapper('first', 'second', 'third'); // 輸出: first second third
概念:Rest 收集 → 處理 → Spread 轉發。這是實務中常見的「記錄」或「代理」模式。
範例 5️⃣ 以 Rest + 元組限制參數結構
// 第一個參數必須是 number,第二個是 string,之後任意
function customPrint(...data: [number, string, ...any[]]): void {
const [id, label, ...rest] = data;
console.log(`ID: ${id}, Label: ${label}`, ...rest);
}
// 正確呼叫
customPrint(101, 'User', { name: 'Alice' }, true);
// 錯誤呼叫(編譯時會報錯)
// customPrint('101', 'User'); // Type 'string' is not assignable to type 'number'.
重點:使用 元組型別 可以在編譯階段捕捉「參數順序」與「型別」的錯誤,提升程式安全性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| Rest 必須在最後 | 嘗試在中間使用 Rest 會產生語法錯誤。 | 永遠把 ...args 放在參數列表最後。 |
空陣列的 reduce |
未提供初始值時,reduce 在空陣列會拋例外。 |
提供初始值(如 0、[])或先檢查長度。 |
| 型別寬鬆導致隱藏錯誤 | 若使用 any[],會失去 TypeScript 的型別保護。 |
盡量使用具體型別(number[]、[number, string])或 泛型。 |
| Spread 位置錯誤 | 在呼叫函式時忘記使用 Spread,導致參數被視為單一陣列。 | 確認 fn(...arr) 而非 fn(arr)。 |
| 過度使用 Rest | 把所有參數都收進 Rest,會讓 API 不易閱讀。 | 只在需要可變長度參數時使用,其餘情況保持明確的參數列表。 |
最佳實踐
- 明確型別:盡量為 Rest 參數加上具體的陣列或元組型別,避免使用
any[]。 - 預設值:如果函式內部會對陣列做累加或迭代,提供合理的預設值或檢查長度。
- 函式說明文件:在 JSDoc 或 TSDoc 中說明 Rest 參數的意圖與期望的型別,提升可讀性。
- 結合泛型:在需要「保留每個元素型別」的情況下,使用泛型 Rest(
<T extends unknown[]>),讓呼叫者得到更精確的回傳型別。 - 測試覆蓋:撰寫單元測試,尤其是「空參數」與「大量參數」的邊界情況,確保函式在不同輸入下行為一致。
實際應用場景
日誌系統
需要接受任意數量的訊息或物件,再一次性寫入檔案或遠端服務。function log(...entries: (string | object)[]) { // 整理後一次送出 }API 請求封裝
某些 API 允許一次傳入多筆資料(批次新增),使用 Rest 收集資料陣列即可。async function batchCreate<T>(endpoint: string, ...items: T[]) { return fetch(endpoint, { method: 'POST', body: JSON.stringify(items) }); }函式柯里化(Currying)
透過 Rest 收集中間參數,返回新函式繼續收集剩餘參數。function curry(fn: (...args: any[]) => any) { return function curried(...args1: any[]) { if (args1.length >= fn.length) { return fn(...args1); } return (...args2: any[]) => curried(...args1, ...args2); }; }React 組件的 Props 傳遞
在高階組件(HOC)中,使用 Rest 收集所有傳入的 props,並透過 Spread 傳給包裝的子組件。const withLogger = <P extends object>(Component: React.ComponentType<P>) => { return (props: P) => { console.log('Props:', props); return <Component {...props} />; }; };指令列工具(CLI)
Node.js 程式常使用process.argv.slice(2)取得使用者輸入的參數,配合 Rest 解析成指令。function cli(...args: string[]) { // args[0] 為子指令,後續為選項 } cli(...process.argv.slice(2));
總結
Rest 參數是 TypeScript 中處理不定長度參數的關鍵工具。它不僅讓函式具備彈性,也因為 型別系統的加持,能在編譯階段捕捉錯誤、保留每個參數的型別資訊。掌握以下要點,即可在日常開發中安全、有效地使用 Rest:
- 語法必須放在最後,且只能有一個。
- 為 Rest 參數 指定具體型別(陣列或元組),盡量避免
any[]。 - 結合 泛型 可保留每個元素的精確型別,提升 API 可讀性。
- Spread 與 Rest 常常配合使用,記得在呼叫時正確展開陣列。
- 注意 空陣列的操作(如
reduce),提供預設值或事前檢查。
透過上述概念與範例,你已經具備在 實務專案 中使用 Rest 參數的能力。無論是日誌、API 批次處理、函式柯里化或是 CLI 工具,Rest 都能讓你的程式碼更具彈性與可維護性。祝你在 TypeScript 的旅程中,寫出更乾淨、更安全的函式!