本文 AI 產出,尚未審核

TypeScript 函式(Functions) – Rest 參數

簡介

在 JavaScript 與 TypeScript 中,函式是程式的核心組件。隨著需求的多樣化,我們常常需要 接受不定數量的參數,例如 console.logArray.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 不易閱讀。 只在需要可變長度參數時使用,其餘情況保持明確的參數列表。

最佳實踐

  1. 明確型別:盡量為 Rest 參數加上具體的陣列或元組型別,避免使用 any[]
  2. 預設值:如果函式內部會對陣列做累加或迭代,提供合理的預設值或檢查長度。
  3. 函式說明文件:在 JSDoc 或 TSDoc 中說明 Rest 參數的意圖與期望的型別,提升可讀性。
  4. 結合泛型:在需要「保留每個元素型別」的情況下,使用泛型 Rest(<T extends unknown[]>),讓呼叫者得到更精確的回傳型別。
  5. 測試覆蓋:撰寫單元測試,尤其是「空參數」與「大量參數」的邊界情況,確保函式在不同輸入下行為一致。

實際應用場景

  1. 日誌系統
    需要接受任意數量的訊息或物件,再一次性寫入檔案或遠端服務。

    function log(...entries: (string | object)[]) {
      // 整理後一次送出
    }
    
  2. API 請求封裝
    某些 API 允許一次傳入多筆資料(批次新增),使用 Rest 收集資料陣列即可。

    async function batchCreate<T>(endpoint: string, ...items: T[]) {
      return fetch(endpoint, { method: 'POST', body: JSON.stringify(items) });
    }
    
  3. 函式柯里化(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);
      };
    }
    
  4. 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} />;
      };
    };
    
  5. 指令列工具(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 可讀性。
  • SpreadRest 常常配合使用,記得在呼叫時正確展開陣列。
  • 注意 空陣列的操作(如 reduce),提供預設值或事前檢查。

透過上述概念與範例,你已經具備在 實務專案 中使用 Rest 參數的能力。無論是日誌、API 批次處理、函式柯里化或是 CLI 工具,Rest 都能讓你的程式碼更具彈性與可維護性。祝你在 TypeScript 的旅程中,寫出更乾淨、更安全的函式!