TypeScript 課程 — 函式(Functions)
主題:函式型別(Function Type)
簡介
在 JavaScript 中,函式是一等公民;它們可以被指派給變數、作為參數傳遞,也能作為回傳值。
TypeScript 為函式加入了靜態型別的概念,使得開發者在編寫、重構與除錯時能得到更好的安全性與自動完成支援。
掌握 函式型別(Function Type)不只是讓程式碼更嚴謹,更能提升團隊合作時的可讀性與維護性。無論是撰寫簡單的回呼(callback)還是設計複雜的高階函式,正確描述函式的參數與回傳型別都是關鍵。
核心概念
1️⃣ 基本函式型別語法
在 TypeScript 中,函式型別的寫法有兩種常見形式:
// 方法一:使用括號形式 (參數型別) => 回傳型別
type Adder = (a: number, b: number) => number;
// 方法二:使用介面 (interface) 定義呼叫簽名
interface Greeter {
(msg: string): void;
}
- 左邊的 type 或 interface 代表「這是一個函式」的型別別名。
- 括號內列出所有參數的型別,最後以
=>指定回傳型別。 - 介面方式可同時加入其他屬性(如
description: string),適合描述「函式物件」的情境。
2️⃣ 可選參數與預設值
函式型別同樣支援可選參數 (?) 與預設值 (=):
type Logger = (message: string, level?: 'info' | 'warn' | 'error') => void;
const log: Logger = (msg, level = 'info') => {
console[level](msg);
};
level?表示呼叫時可以省略第二個參數。- 若在實作時提供預設值,呼叫者即使省略參數也會得到預期的行為。
3️⃣ 參數的剩餘 (Rest) 與陣列型別
當函式需要接受不定數量的參數時,可使用 rest 參數:
type SumAll = (...nums: number[]) => number;
const sumAll: SumAll = (...nums) => nums.reduce((a, b) => a + b, 0);
...nums: number[]表示「任意多個 number」會被收集成一個陣列。- 這在實作 可變長度的 API(例如
Math.max)時非常有用。
4️⃣ this 參數的型別
在嚴格模式下,this 的型別不會自動推斷。可以在函式型別中顯式宣告:
type Point = { x: number; y: number };
type Move = (this: Point, dx: number, dy: number) => void;
const move: Move = function (dx, dy) {
this.x += dx;
this.y += dy;
};
const p: Point = { x: 0, y: 0 };
move.call(p, 5, 3); // p => { x: 5, y: 3 }
this: Point告訴編譯器this必須是Point型別,防止因誤用this而產生的執行時錯誤。
5️⃣ 函式重載(Overloads)
TypeScript 允許同一個函式根據參數型別提供多個呼叫簽名:
// 兩個重載簽名
function format(value: number): string;
function format(value: Date, formatStr?: string): string;
// 真正的實作
function format(value: any, formatStr?: string): string {
if (typeof value === 'number') {
return value.toFixed(2);
}
// Date 處理
const fmt = formatStr ?? 'YYYY-MM-DD';
// 假設有 dayjs 之類的庫
return (value as Date).toISOString().slice(0, 10);
}
- 呼叫者只能看到定義的簽名,避免傳入不支援的參數。
- 實作本身必須兼容所有簽名,通常使用
any或聯合型別作為最終參數型別。
程式碼範例
以下提供 5 個實務中常見的函式型別範例,每個範例都附有說明註解,幫助你快速上手。
範例 1:簡易計算器函式型別
// 定義一個接受兩個 number,回傳 number 的函式型別
type Calculator = (a: number, b: number) => number;
// 實作四則運算
const add: Calculator = (a, b) => a + b;
const sub: Calculator = (a, b) => a - b;
const mul: Calculator = (a, b) => a * b;
const div: Calculator = (a, b) => b !== 0 ? a / b : NaN;
重點:使用
type讓每個運算子都有相同的簽名,方便在集合中統一管理。
範例 2:事件監聽器的回呼型別
// 只要符合 (event: MouseEvent) => void 的函式都能當作 click handler
type ClickHandler = (event: MouseEvent) => void;
const button = document.createElement('button');
button.textContent = '點我';
const handleClick: ClickHandler = (e) => {
console.log('Clicked at', e.clientX, e.clientY);
};
button.addEventListener('click', handleClick);
document.body.appendChild(button);
技巧:把 DOM 事件的回呼抽離成型別,可在多個元件間重複使用,減少錯誤。
範例 3:高階函式 – filter 的型別
// 泛型函式型別,接受陣列 T[] 與 (item: T) => boolean,回傳 T[]
type FilterFn = <T>(arr: T[], predicate: (item: T) => boolean) => T[];
const filter: FilterFn = (arr, predicate) => arr.filter(predicate);
// 使用範例
const nums = [1, 2, 3, 4, 5];
const even = filter(nums, n => n % 2 === 0); // [2,4]
說明:透過 泛型 (
<T>) 讓filter能適用於任意型別的陣列,保持型別安全。
範例 4:帶 this 的物件方法型別
type Counter = {
count: number;
// this 必須是 Counter 本身
inc: (this: Counter, step?: number) => void;
};
const counter: Counter = {
count: 0,
inc(this, step = 1) {
this.count += step;
},
};
counter.inc(); // count => 1
counter.inc(5); // count => 6
注意:若不寫
this: Counter,this會被推斷為any,失去型別保護。
範例 5:API 客戶端的回傳 Promise 型別
// 定義一個接受 URL 並回傳 Promise<T> 的函式型別
type Fetcher = <T = any>(url: string) => Promise<T>;
const fetchJson: Fetcher = async (url) => {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
};
// 使用方式
interface User {
id: number;
name: string;
}
fetchJson<User>('https://api.example.com/users/1')
.then(user => console.log(user.name));
實務:在大型前端專案中,將所有 API 呼叫統一以
Fetcher型別描述,可讓 IDE 自動推斷回傳結構,減少手寫型別的繁瑣。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記標註 this |
在物件方法裡使用 this 卻未宣告型別,會導致 any,失去型別檢查。 |
使用 (this: MyType, ...) => 或在介面中加入呼叫簽名。 |
過度使用 any |
為了快速寫完函式而把參數或回傳值寫成 any,會讓 TypeScript 的好處消失。 |
儘量使用 泛型 或 聯合型別,必要時才使用 unknown 再做型別斷言。 |
| 函式重載實作不完整 | 實作只符合部分重載簽名,導致呼叫時仍出錯。 | 在實作函式最後使用最寬鬆的型別(any、unknown)作參數,並在函式內部自行檢查。 |
| Rest 參數與普通參數混用錯誤 | ...args: number[] 必須放在參數列表最後,否則會編譯錯誤。 |
確保 Rest 參數是最後一個參數,或使用 元組型別 進行更精細的控制。 |
| 回呼函式的返回值被忽略 | 許多 API(如 Array.map)期待回呼回傳值,寫成 void 會導致型別不匹配。 |
明確指定回呼的回傳型別,或使用 as const 固定回傳值的型別。 |
最佳實踐
- 使用型別別名 (
type) 為函式建立可重用的簽名。 - 在公共 API 中暴露介面 (
interface),讓未來可以擴充屬性。 - 盡量使用泛型,提升函式的通用性與型別安全。
- 為
this明確標註,避免隱式any。 - 寫測試:函式型別錯誤往往在執行時才顯現,單元測試能補足編譯階段的盲點。
實際應用場景
Redux / NgRx 中的 reducer
type Reducer<S, A> = (state: S, action: A) => S;透過
Reducer型別,所有 reducer 必須遵守相同的簽名,讓 store 組合更安全。React 事件處理
type OnChange = (e: React.ChangeEvent<HTMLInputElement>) => void; const handleChange: OnChange = e => setValue(e.target.value);把 React 事件的回呼抽成型別,可在多個元件間共享,減少重複程式碼。
Node.js 中的中介層 (middleware)
type Middleware = (req: Request, res: Response, next: () => void) => void;定義
Middleware後,所有自訂中介層都必須符合此型別,避免因參數順序錯誤導致的錯誤。函式式程式設計 (Functional Programming)
使用Compose、Pipe等高階函式時,需要明確描述每個函式的輸入與輸出型別,才能在編譯階段捕捉組合錯誤。微服務間的 RPC 呼叫
為每個遠端方法定義type RpcMethod = (payload: Request) => Promise<Response>,確保客戶端與伺服器的介面保持同步。
總結
函式型別是 TypeScript 中最具威力的特性之一。透過明確的參數與回傳型別,我們可以:
- 減少執行時錯誤:編譯階段即捕捉不匹配的呼叫。
- 提升開發效率:IDE 能提供自動完成與即時提示。
- 增進程式碼可讀性:型別別名與介面讓函式簽名一目了然。
- 支援大型專案:統一的函式型別讓團隊協作更順暢,未來擴充也更安全。
從簡單的回呼函式到高階的泛型函式,熟練掌握 Function Type,你將能在任何 TypeScript 專案中寫出更健壯、更易維護的程式碼。祝你在 Typescript 的旅程中,寫出乾淨、可靠的函式! 🚀