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 參數的型別
在某些情境(例如陣列的 map、filter)中,回呼函式會接收一個隱式的 this 參數。TypeScript 允許在函式型別的最前面明確宣告 this 的型別,避免因 this 指向錯誤而產生的執行時問題。
type ThisCallback = (this: Date, value: string) => string;
範例 4:自訂 Array.prototype.map 的 thisArg
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 或箭頭函式,導致 this 為 undefined。 |
為回呼型別加入 this 參數,或使用 箭頭函式(自動綁定外層 this)。 |
回呼返回 any |
沒有明確指定回傳型別,導致後續使用回傳值時失去型別提示。 | 為回呼型別加上正確的回傳型別,例如 => number、=> Promise<void>。 |
過度使用 any |
為了省事直接寫 cb: any,失去 TypeScript 的好處。 |
盡量使用 具體的函式型別,或使用 泛型 讓型別保持彈性。 |
| 回呼嵌套(Callback Hell) | 多層非同步回呼寫成巢狀結構,程式難以閱讀。 | 考慮改用 Promise、async/await,或將回呼抽離成獨立函式。 |
最佳實踐
- 明確定義回呼型別:在函式簽名中直接使用
type或interface,讓 IDE 能提供自動補全與錯誤檢查。 - 使用
readonly:若回呼不會改變傳入的參數,使用readonly修飾提升不可變性。 - 盡量避免
any:即使暫時不確定型別,也可以先使用泛型unknown,再在實作中做型別保護。 - 把回呼抽成可重用的型別:在大型專案中,將常見的回呼(如
ErrorCallback,SuccessCallback)集中管理,提升一致性。 - 結合 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.map、reduce、filter)皆接受回呼。自訂高階函式時,使用泛型回呼可讓函式在不同型別間自由切換。
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 的世界裡玩得開心,寫出更安全、更可靠的程式碼! 🚀