本文 AI 產出,尚未審核

TypeScript → 函式(Functions) → 回呼函式(Callback Type)


簡介

在前端開發或 Node.js 後端程式中,回呼函式(callback) 是一種極常見的設計模式。它讓我們可以把「要在某個時機執行的程式碼」以參數的形式傳遞給另一個函式,等到條件滿足或非同步操作完成時再呼叫。
在純 JavaScript 中,回呼函式的型別是「any」,容易寫出執行時錯誤的程式碼;而 TypeScript 則提供了靜態型別系統,使我們能在編譯階段就捕捉到參數、回傳值、this 位置等錯誤,提升程式的可讀性與可靠性。

本篇文章將從 概念、語法、實作範例,一路闡述如何在 TypeScript 中正確定義與使用回呼函式,並分享常見陷阱與最佳實踐,協助你在日常開發中寫出安全、易維護的程式碼。


核心概念

1. 基本的 Callback 型別

在 TypeScript 中,回呼函式本質上就是 函式型別(function type)。最簡單的寫法是使用箭頭函式語法描述參數與回傳型別:

type SimpleCallback = (msg: string) => void;
  • msg: string → 回呼函式會接收到一個字串參數。
  • => void → 回呼函式不回傳值(void),如果有回傳值則寫實際型別。

範例 1:最簡單的回呼使用

function greet(name: string, cb: SimpleCallback): void {
  const message = `哈囉,${name}!`;
  cb(message); // 呼叫回呼函式
}

// 呼叫方
greet('小明', (msg) => {
  console.log('回呼收到訊息:', msg);
});

重點cb 的型別已在 SimpleCallback 中固定,若傳入的函式簽名不符合(例如缺少參數或回傳非 void),編譯器會直接報錯。


2. 多參數與回傳值的 Callback

實務上常見的回呼會帶有多個參數,甚至會回傳結果供呼叫端使用。只要在型別定義中列出所有參數與回傳型別即可。

type MathOperation = (a: number, b: number) => number;

範例 2:計算器的回呼

function calculate(a: number, b: number, op: MathOperation): number {
  return op(a, b);
}

// 加法回呼
const add: MathOperation = (x, y) => x + y;

// 測試
const sum = calculate(5, 3, add); // 8
console.log('5 + 3 =', sum);

如果直接傳入不符合 MathOperation 的函式(例如只接受一個參數),TypeScript 會在編譯階段提示錯誤。


3. 可選參數與預設值的 Callback

有時回呼函式的參數不一定全部需要,這時可以使用 可選參數 (?) 或 預設值 來提高彈性。

type OptionalCallback = (msg?: string, code: number = 0) => void;

範例 3:錯誤處理的回呼

function fetchData(url: string, onError: OptionalCallback): void {
  // 模擬非同步請求失敗
  const errorMsg = '網路連線失敗';
  const errorCode = 503;
  onError(errorMsg, errorCode);
}

// 呼叫方只想接收錯誤代碼
fetchData('https://api.example.com', (_, code) => {
  console.warn('收到錯誤代碼:', code);
});

技巧:在回呼的型別裡使用 ?,呼叫方可以選擇傳入或不傳入對應參數;若使用預設值,呼叫方可省略傳遞該參數。


4. this 參數的型別

在某些情境(例如陣列的 mapfilter)中,回呼函式會接收一個隱式的 this 參數。TypeScript 允許在函式型別的最前面明確宣告 this 的型別,避免因 this 指向錯誤而產生的執行時問題。

type ThisCallback = (this: Date, value: string) => string;

範例 4:自訂 Array.prototype.mapthisArg

function mapWithThis<T, U>(arr: T[], cb: ThisCallback, thisArg: Date): U[] {
  return arr.map(function (this: Date, item) {
    // 這裡的 this 會自動被推斷為 Date
    return cb.call(this, item);
  }, thisArg);
}

// 使用範例
const words = ['apple', 'banana', 'cherry'];
const result = mapWithThis(words, function (this: Date, w) {
  return `${w.toUpperCase()} - ${this.getFullYear()}`;
}, new Date());

console.log(result);
// → ['APPLE - 2025', 'BANANA - 2025', 'CHERRY - 2025']

重點:若回呼函式沒有正確宣告 this 型別,this 會被推斷為 any,失去型別安全的好處。


5. 泛型 Callback

當回呼函式需要根據不同情況接受或回傳不同型別時,泛型(generics)提供了極大的彈性。

type Mapper<T, U> = (item: T) => U;

範例 5:通用的資料轉換函式

function transformArray<T, U>(source: T[], mapper: Mapper<T, U>): U[] {
  return source.map(mapper);
}

// 例子 1:把數字轉成字串
const numbers = [1, 2, 3];
const strings = transformArray(numbers, n => `No.${n}`);
console.log(strings); // ['No.1', 'No.2', 'No.3']

// 例子 2:把使用者物件轉成姓名字串
interface User { id: number; name: string; age: number; }
const users: User[] = [{ id: 1, name: '小王', age: 28 }];
const names = transformArray(users, u => u.name);
console.log(names); // ['小王']

使用泛型的好處在於,呼叫方不需要手動指定型別,編譯器會根據傳入的參數自動推斷,保持型別安全且程式碼更具可讀性。


常見陷阱與最佳實踐

陷阱 說明 解決方式
回呼的參數型別不匹配 直接傳入 (msg) => console.log(msg),但實際回呼需要兩個參數 (msg, code) 時,會產生未使用參數的錯誤或執行時 undefined 在型別定義時使用 可選參數 (?) 或 預設值,或在呼叫方明確寫出全部參數。
this 指向錯誤 在普通函式裡使用 this,卻忘記使用 call/apply 或箭頭函式,導致 thisundefined 為回呼型別加入 this 參數,或使用 箭頭函式(自動綁定外層 this)。
回呼返回 any 沒有明確指定回傳型別,導致後續使用回傳值時失去型別提示。 為回呼型別加上正確的回傳型別,例如 => number=> Promise<void>
過度使用 any 為了省事直接寫 cb: any,失去 TypeScript 的好處。 盡量使用 具體的函式型別,或使用 泛型 讓型別保持彈性。
回呼嵌套(Callback Hell) 多層非同步回呼寫成巢狀結構,程式難以閱讀。 考慮改用 Promiseasync/await,或將回呼抽離成獨立函式。

最佳實踐

  1. 明確定義回呼型別:在函式簽名中直接使用 typeinterface,讓 IDE 能提供自動補全與錯誤檢查。
  2. 使用 readonly:若回呼不會改變傳入的參數,使用 readonly 修飾提升不可變性。
  3. 盡量避免 any:即使暫時不確定型別,也可以先使用泛型 unknown,再在實作中做型別保護。
  4. 把回呼抽成可重用的型別:在大型專案中,將常見的回呼(如 ErrorCallback, SuccessCallback)集中管理,提升一致性。
  5. 結合 Promise 時,提供雙重介面:如果函式同時支援回呼與 Promise,使用函式重載(function overload)提供兩種呼叫方式。
// 同時支援 callback 與 Promise
function loadJson(url: string, cb?: (data: any) => void): Promise<any> | void {
  if (cb) {
    fetch(url).then(r => r.json()).then(cb);
    return;
  }
  return fetch(url).then(r => r.json());
}

實際應用場景

1. 事件處理(Event Handlers)

在瀏覽器或 UI 框架(React、Vue)中,事件監聽器本質上就是回呼函式。使用 TypeScript 時,我們可以為事件回呼寫出精確的型別,避免因傳入錯誤參數導致的執行時錯誤。

// React 範例(使用 TypeScript)
type ClickHandler = (e: React.MouseEvent<HTMLButtonElement>) => void;

const MyButton: React.FC = () => {
  const handleClick: ClickHandler = (e) => {
    console.log('按鈕被點擊,座標:', e.clientX, e.clientY);
  };
  return <button onClick={handleClick}>點我</button>;
};

2. 非同步 API 呼叫

舊式的 Node.js API(如 fs.readFile) 仍以回呼為主。透過正確的回呼型別,我們可以在遷移到 Promise 前,先確保型別安全。

import { readFile } from 'fs';

type ReadCallback = (err: NodeJS.ErrnoException | null, data: Buffer) => void;

readFile('data.txt', (err, data) => {
  if (err) {
    console.error('讀檔失敗:', err);
    return;
  }
  console.log('檔案內容:', data.toString());
});

3. 高階函式(Higher‑Order Functions)

許多函式式程式設計的工具(如 Array.mapreducefilter)皆接受回呼。自訂高階函式時,使用泛型回呼可讓函式在不同型別間自由切換。

function groupBy<T, K extends keyof any>(arr: T[], keyFn: (item: T) => K): Record<K, T[]> {
  return arr.reduce((acc, cur) => {
    const key = keyFn(cur);
    (acc[key] ??= []).push(cur);
    return acc;
  }, {} as Record<K, T[]>);
}

// 範例
interface Product { id: number; category: string; price: number; }
const products: Product[] = [
  { id: 1, category: '書籍', price: 120 },
  { id: 2, category: '電子', price: 3500 },
  { id: 3, category: '書籍', price: 80 },
];
const byCategory = groupBy(products, p => p.category);
console.log(byCategory);

4. 中介軟體(Middleware)與插件機制

在 Express、Koa 等框架中,中介軟體 本身就是一串回呼。定義統一的 Middleware 型別可以讓插件開發者遵守相同的簽名。

import { Request, Response, NextFunction } from 'express';

type Middleware = (req: Request, res: Response, next: NextFunction) => void;

const logger: Middleware = (req, _res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
};

總結

  • 回呼函式 是 JavaScript/TypeScript 中處理非同步、事件與高階函式的核心概念。
  • 在 TypeScript 裡,我們可以透過 函式型別、可選參數、this 型別、泛型 等語法,為回呼提供完整的靜態檢查,避免執行時錯誤。
  • 常見的陷阱包括參數型別不匹配、this 指向錯誤以及過度使用 any,解決方式是明確宣告型別、使用可選參數或預設值、善用泛型
  • 實務上,回呼廣泛應用於 事件處理、非同步 API、資料轉換、Middleware 等情境。掌握正確的型別寫法,能讓程式碼在大型專案中保持可讀、可維護且不易出錯。

實務建議:在新專案中,先建立一套共用的回呼型別(如 ErrorCallback<T>SuccessCallback<T>),並在 ESLint/TSLint 中加入「禁止使用 any」的規則,這樣即使是初學者也能在開發過程中自然養成良好的型別意識。

祝你在 TypeScript 的世界裡玩得開心,寫出更安全、更可靠的程式碼! 🚀