本文 AI 產出,尚未審核

TypeScript 函式(Functions)— 預設值參數

簡介

在日常開發中,我們常會遇到需要為函式參數提供「預設值」的情況。若呼叫端忘記傳入某個參數,函式仍能以合理的預設行為繼續執行,這不僅提升程式的容錯性,也能讓 API 更易於使用。

TypeScript 中,預設值參數(default parameters)不只是 JavaScript 的語法糖,更結合了型別系統的檢查,使得開發者在編寫與呼叫函式時能得到更完整的 IntelliSense 與編譯期錯誤提示。本文將從語法到實務應用,完整說明如何在 TypeScript 中善用預設值參數。


核心概念

1. 基本語法

在函式宣告的參數列表中,直接以 = 指定預設值即可。若呼叫時省略該參數,TypeScript 會自動使用預設值。

function greet(name: string = "訪客"): string {
  return `哈囉,${name}!`;
}

// 呼叫方式
greet();            // => "哈囉,訪客!"
greet("小明");      // => "哈囉,小明!"
  • name: string = "訪客" 同時描述了參數的型別與預設值。
  • 若傳入 undefined,也會觸發預設值的使用;傳入 null 則不會。
greet(undefined); // 使用預設值 => "哈囉,訪客!"
greet(null);      // 直接傳入 null => "哈囉,null!"

2. 預設值與型別推斷

若預設值本身能明確推斷出型別,參數的型別可以省略,TypeScript 會自動推斷。

function add(a: number, b = 0): number {
  return a + b;
}

// b 的型別被推斷為 number
add(5);          // => 5
add(5, 3);       // => 8

小技巧:在大型函式中,建議仍明確寫出型別,避免未來維護時產生歧義。

3. 多個參數的預設值

預設值參數可以出現在任意位置,但若在 非最後 的參數使用,呼叫端必須顯式傳入 undefined 以觸發預設值,否則後面的參數會被錯位。

function createUser(name: string, isAdmin: boolean = false, age?: number) {
  return { name, isAdmin, age };
}

// 正確使用預設值
createUser("阿美");                    // isAdmin = false
createUser("阿美", undefined, 30);    // isAdmin = false, age = 30

// 錯誤示範:省略 isAdmin 直接傳入 age,會被當作 isAdmin
createUser("阿美", 30); // 編譯錯誤:Argument of type 'number' is not assignable to parameter of type 'boolean'.

建議:將所有有預設值的參數放在參數列表的最後,或使用 物件解構 方式傳遞參數(下節會說明)。

4. 物件解構與預設值

在許多 API 設計中,會以單一「設定物件」的方式傳遞多個參數。結合解構與預設值,可同時解決參數順序與可選性的問題。

interface FetchOptions {
  url: string;
  method?: "GET" | "POST";
  timeout?: number;
}

function fetchData({ url, method = "GET", timeout = 5000 }: FetchOptions): Promise<string> {
  // 假裝呼叫 fetch...
  return Promise.resolve(`Fetched ${url} with ${method} (timeout ${timeout}ms)`);
}

// 呼叫方式
fetchData({ url: "/api/users" }); // method = GET, timeout = 5000
fetchData({ url: "/api/users", method: "POST" }); // timeout = 5000
  • 解構的參數本身已是 必填url),其他則有預設值。
  • 呼叫端只需要提供需要改變的欄位,省去繁瑣的參數順序。

5. 預設值與函式重載(overload)

有時候想要根據不同的參數組合提供不同的回傳型別,這時可以結合 函式重載預設值

// 重載簽名
function format(value: number, locale?: string): string;
function format(value: Date, locale?: string): string;

// 實作
function format(value: number | Date, locale: string = "en-US"): string {
  if (typeof value === "number") {
    return new Intl.NumberFormat(locale).format(value);
  } else {
    return new Intl.DateTimeFormat(locale).format(value);
  }
}

// 使用
format(1234567);               // "1,234,567"
format(new Date(), "zh-TW");   // "2025/11/19"
  • 預設值 locale = "en-US" 只在未提供時套用。
  • 重載讓編譯器在呼叫時能正確推斷回傳型別。

程式碼範例

範例 1:簡易計算器

function calculate(
  a: number,
  b: number,
  operator: "+" | "-" | "*" | "/" = "+"
): number {
  switch (operator) {
    case "+": return a + b;
    case "-": return a - b;
    case "*": return a * b;
    case "/": return a / b;
  }
}

// 預設使用加法
console.log(calculate(5, 3));          // 8
console.log(calculate(5, 3, "*"));    // 15

範例 2:日誌函式(支援可選的等級)

type LogLevel = "debug" | "info" | "warn" | "error";

function log(message: string, level: LogLevel = "info"): void {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
}

log("系統啟動");                // 預設 info
log("資料庫連線失敗", "error");

範例 3:API 請求封裝(使用解構)

interface RequestConfig {
  url: string;
  method?: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  timeout?: number;
}

function request({
  url,
  method = "GET",
  headers = { "Content-Type": "application/json" },
  timeout = 3000,
}: RequestConfig): Promise<Response> {
  return fetch(url, { method, headers })
    .then(res => {
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return res;
    })
    .catch(err => {
      console.error(`Request failed: ${err.message}`);
      throw err;
    });
}

// 呼叫
request({ url: "/api/items" }); // GET, 預設 header & timeout
request({ url: "/api/items", method: "POST", timeout: 5000 });

範例 4:函式重載 + 預設值(格式化)

function format(value: number, locale?: string): string;
function format(value: Date, locale?: string): string;
function format(value: number | Date, locale: string = "en-US"): string {
  return typeof value === "number"
    ? new Intl.NumberFormat(locale).format(value)
    : new Intl.DateTimeFormat(locale).format(value);
}

// 使用
console.log(format(123456));          // "123,456"
console.log(format(new Date(), "ja-JP"));

範例 5:可變參數與預設值的結合

function concat(separator: string = ", ", ...items: string[]): string {
  return items.join(separator);
}

console.log(concat(" - ", "A", "B", "C")); // "A - B - C"
console.log(concat(undefined, "X", "Y")); // 使用預設分隔符 => "X, Y"

常見陷阱與最佳實踐

陷阱 說明 解決方式
預設值放在非最後參數 呼叫時若省略中間參數,後面的參數會被錯位。 將所有預設值參數搬到最後,或改用 物件解構
使用 null 代替 undefined null 會直接傳給函式,不會觸發預設值。 若想使用預設值,請傳 undefined 或省略參數。
預設值是物件或陣列時共享引用 預設值在函式定義時只建立一次,會導致多次呼叫共享同一個物件。 使用 函式返回的工廠 或在函式內部 = []= (() => [])() 以產生新實例。
預設值與泛型結合時失去型別推斷 預設值可能會讓 TypeScript 無法正確推斷泛型。 明確指定泛型或使用 條件型別 來保留推斷。
過度依賴預設值導致 API 不易閱讀 太多預設值會讓呼叫端不清楚實際行為。 在文件或 JSDoc 中說明每個預設值的意圖,並盡量保持 簡潔

最佳實踐

  1. 預設值放最後:保持函式簽名的可預測性。
  2. 使用解構物件:解決多參數、可選參數的順序問題。
  3. 避免可變物件作為預設值:使用工廠函式或 Object.assign({}, defaultObj)
  4. 為預設值加上 JSDoc:提升 IDE 裡的說明與自動完成。
  5. 結合型別別名或介面:讓預設值的型別更清晰,減少維護成本。

實際應用場景

  1. 設定檔與套件初始化
    大多數第三方套件會接受一個「設定物件」作為參數,透過預設值讓使用者只必須提供需要自訂的部分。

    initLibrary({ apiKey: "xxx" }); // 只提供必要欄位,其餘使用預設
    
  2. 表單驗證函式
    驗證規則常有「是否必填」等可選屬性,預設值可以讓驗證器在未傳入時自動使用慣用規則。

  3. API 請求的重試機制
    maxRetriesbackoff 等參數可設為預設值,讓呼叫端在大多數情況下不必自行設定。

  4. 日誌與監控
    預設的日誌等級、輸出格式、目的地(console、file)均可透過預設值簡化初始化流程。

  5. 多語系或在地化
    函式接受 locale 參數時,將 locale = "en-US" 作為預設,可讓程式在未提供語系時仍能正常運作。


總結

預設值參數是 TypeScript 中提升函式彈性與可讀性的關鍵特性。透過簡潔的語法,我們可以:

  • 為可選參數提供安全的預設行為,減少 undefined 錯誤。
  • 結合 物件解構,解決參數順序與可選性的痛點。
  • 函式重載泛型 以及 可變參數 中靈活運用。

在實務開發中,遵守「預設值放最後」的原則、避免共享可變物件、並以 JSDoc 或介面說明每個預設值的意圖,能讓程式碼更易於維護與擴充。希望本篇文章能幫助你在日常開發裡,寫出更安全直觀具備彈性的 TypeScript 函式。祝你寫程式快樂!