TypeScript – 函式(Functions)
主題:函式重載(Function Overload)
簡介
在大型前端或 Node.js 專案中,函式的可讀性與彈性往往直接影響到維護成本。
JavaScript 本身不支援函式簽名的多樣化,導致同一個函式在不同參數情況下只能以「檢查型別」或「可變長度參數」的方式處理,程式碼容易變得雜亂且缺乏自文件(self‑documenting)特性。
TypeScript 為 JavaScript 加上靜態型別系統,提供了 函式重載(Function Overload) 的機制。透過多個宣告簽名,我們可以在編譯階段就讓開發者清楚知道每種呼叫方式所接受的參數與回傳型別,從而提升 IDE 補全、錯誤偵測與程式碼可讀性。
本篇文章將以 淺顯易懂 的方式說明什麼是函式重載、如何正確撰寫、常見陷阱與最佳實踐,並舉出實務上常見的應用情境,幫助從初學者到中階開發者快速上手。
核心概念
1. 為什麼需要函式重載?
- 多樣化的輸入:同一個功能可能接受「字串」或「數字」等不同型別的參數。
- 不同的回傳結果:根據參數的不同,回傳值的型別也會變化(例如搜尋 API 可能回傳單筆或陣列)。
- 提升開發體驗:IDE 能在編寫程式時即時提示正確的參數型別與回傳型別,減少執行時錯誤。
2. 基本語法
在 TypeScript 中,函式重載是由多個簽名(overload signatures)加上一個實作簽名(implementation signature)組成。
// 1️⃣ 重載簽名(只描述參數與回傳型別,不能有實作)
// functionName(param1: Type1, param2: Type2): ReturnType;
// 2️⃣ 實作簽名(唯一一個,包含實際的程式碼)
// function functionName(param: any, ...): any {
// // 實作
// }
注意:重載簽名的順序不影響執行結果,但會影響 TypeScript 的匹配優先級,較具體的簽名應寫在前面。
3. 範例一:簡單的加法函式
// 重載簽名
function add(a: number, b: number): number;
function add(a: string, b: string): string;
// 實作簽名
function add(a: any, b: any): any {
return a + b;
}
// 使用
const sum = add(3, 5); // 8 (number)
const concat = add('a', 'b'); // "ab"(string)
第一個簽名只接受兩個 number,回傳 number;第二個簽名只接受兩個 string,回傳 string。實作簽名使用 any,因為 TypeScript 只會根據呼叫時的型別去匹配前面的重載簽名。
4. 範例二:支援可選參數與預設值
// 重載簽名
function formatDate(date: Date): string;
function formatDate(date: Date, locale: string): string;
// 實作簽名
function formatDate(date: Date, locale?: string): string {
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
return new Intl.DateTimeFormat(locale ?? 'en-US', options).format(date);
}
// 使用
formatDate(new Date()); // "12/31/2023"
formatDate(new Date(), 'zh-TW'); // "2023/12/31"
透過 locale? 表示第二個參數是可選的,實作裡使用了空值合併運算子 ?? 來提供預設值。
5. 範例三:回傳不同型別的 API 包裝
type User = { id: number; name: string };
type Product = { sku: string; price: number };
function fetchEntity(id: number): Promise<User>;
function fetchEntity(sku: string): Promise<Product>;
// 實作簽名
async function fetchEntity(key: number | string): Promise<User | Product> {
if (typeof key === 'number') {
const res = await fetch(`/api/users/${key}`);
return (await res.json()) as User;
} else {
const res = await fetch(`/api/products/${key}`);
return (await res.json()) as Product;
}
}
// 使用
const userPromise = fetchEntity(42); // Promise<User>
const prodPromise = fetchEntity('A001'); // Promise<Product>
此例展示了 根據參數型別返回不同的 Promise,在呼叫端可直接取得正確的型別資訊,避免 any 或手動型別斷言。
6. 範例四:函式重載結合泛型(進階)
// 重載簽名
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[];
function mapArray<T>(arr: T[], fn: (item: T) => T): T[];
// 實作簽名
function mapArray<T, U>(arr: T[], fn: (item: T) => U | T): (U | T)[] {
return arr.map(fn);
}
// 使用
const nums = mapArray([1, 2, 3], n => n * 2); // number[]
const strs = mapArray(['a', 'b'], s => s.toUpperCase()); // string[]
第一個簽名允許回傳型別與原始陣列不同,第二個則是「同型」的情況。透過泛型,我們仍能保持型別安全。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 實作簽名過於寬鬆 | 若實作簽名使用 any 或 unknown,IDE 失去提示,且在大型專案中易產生隱藏錯誤。 |
盡量使用 聯合型別 (`number |
| 重載順序不當 | TypeScript 會依序比對簽名,若把「寬鬆」的簽名寫在前面,較具體的簽名會被遮蔽。 | 將最具體的簽名放在最前,最寬鬆的放最後。 |
| 忘記匯入型別 | 重載簽名中使用的介面或型別若未正確匯入,編譯會報錯。 | 確認所有自訂型別都有正確的 import,或使用 declare 先聲明。 |
| 重載簽名與實作不一致 | 實作簽名回傳型別必須能覆蓋所有重載的回傳型別。若不相容會出現錯誤。 | 讓實作回傳的型別是 所有重載回傳型別的聯合 (`A |
| 過度使用重載 | 為每種微小差異都寫重載會讓程式碼難以維護。 | 只在 參數型別或回傳型別有明顯差異 時使用,否則考慮使用 選項物件(options object)或 函式參數的聯合型別。 |
最佳實踐
- 先寫簽名,再寫實作
- 先列出所有可能的呼叫方式,讓 API 設計更清晰。
- 使用具體的型別描述
- 盡量避免
any、unknown,改用聯合型別或自訂介面。
- 盡量避免
- 保持實作簡潔
- 重載的目的在於型別檢查,實作本身仍應保持單一職責(single responsibility)。
- 加入 JSDoc
- 即使有重載,仍建議在函式上方寫說明,讓產生的文件更完整。
- 測試每個重載
- 單元測試中應針對每個簽名寫一次測試,確保行為一致。
實際應用場景
多語系文字格式化
format(value: number, locale: string): string、format(value: Date, locale: string): string等。
資料庫查詢 API
find(id: number): Promise<User>、find(name: string): Promise<User[]>,根據參數型別返回單筆或多筆結果。
UI 元件的屬性設定
- 像是
setSize(width: number, height: number): void與setSize(size: { w: number; h: number }): void,讓使用者可以傳入兩種不同的參數結構。
- 像是
日誌(Logging)函式
log(message: string): void、log(error: Error, context?: string): void,根據傳入的型別自動決定輸出格式。
第三方函式庫的包裝
- 將不支援型別的 JavaScript 套件(例如
moment.js)包裝成 TypeScript 可重載的介面,提升開發體驗。
- 將不支援型別的 JavaScript 套件(例如
總結
- 函式重載是 TypeScript 為 JavaScript 加上的一項強大特性,讓同一個函式可以根據不同參數型別或數量返回不同的結果,同時保持編譯時的型別安全。
- 撰寫時遵循「先寫簽名、後寫實作」的流程,並確保實作簽名能覆蓋所有重載的回傳型別。
- 避免過度使用
any、unknown,適當排列簽名順序,並在每個重載上加入清晰的說明,可大幅提升程式碼的可讀性與維護性。 - 在實務專案中,函式重載常見於多語系、資料查詢、UI 設定與第三方套件包裝等情境,正確運用可讓 API 更具表意性,減少錯誤與重複程式碼。
掌握了 函式重載,你就能在 TypeScript 中寫出更具彈性、易於使用的函式介面,為團隊的開發效率與程式品質奠定堅實基礎。祝你寫程式愉快!