本文 AI 產出,尚未審核

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 固定回傳值的型別。

最佳實踐

  1. 使用型別別名 (type) 為函式建立可重用的簽名。
  2. 在公共 API 中暴露介面 (interface),讓未來可以擴充屬性。
  3. 盡量使用泛型,提升函式的通用性與型別安全。
  4. 為 this 明確標註,避免隱式 any。
  5. 寫測試:函式型別錯誤往往在執行時才顯現,單元測試能補足編譯階段的盲點。

實際應用場景

  1. Redux / NgRx 中的 reducer

    type Reducer<S, A> = (state: S, action: A) => S;
    

    透過 Reducer 型別,所有 reducer 必須遵守相同的簽名,讓 store 組合更安全。

  2. React 事件處理

    type OnChange = (e: React.ChangeEvent<HTMLInputElement>) => void;
    const handleChange: OnChange = e => setValue(e.target.value);
    

    把 React 事件的回呼抽成型別,可在多個元件間共享,減少重複程式碼。

  3. Node.js 中的中介層 (middleware)

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

    定義 Middleware 後,所有自訂中介層都必須符合此型別,避免因參數順序錯誤導致的錯誤。

  4. 函式式程式設計 (Functional Programming)
    使用 Compose、Pipe 等高階函式時,需要明確描述每個函式的輸入與輸出型別,才能在編譯階段捕捉組合錯誤。

  5. 微服務間的 RPC 呼叫
    為每個遠端方法定義 type RpcMethod = (payload: Request) => Promise<Response>,確保客戶端與伺服器的介面保持同步。


總結

函式型別是 TypeScript 中最具威力的特性之一。透過明確的參數與回傳型別,我們可以:

  • 減少執行時錯誤:編譯階段即捕捉不匹配的呼叫。
  • 提升開發效率:IDE 能提供自動完成與即時提示。
  • 增進程式碼可讀性:型別別名與介面讓函式簽名一目了然。
  • 支援大型專案:統一的函式型別讓團隊協作更順暢,未來擴充也更安全。

從簡單的回呼函式到高階的泛型函式,熟練掌握 Function Type,你將能在任何 TypeScript 專案中寫出更健壯、更易維護的程式碼。祝你在 Typescript 的旅程中,寫出乾淨、可靠的函式! 🚀