本文 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;
}
  • 左邊的 typeinterface 代表「這是一個函式」的型別別名。
  • 括號內列出所有參數的型別,最後以 => 指定回傳型別。
  • 介面方式可同時加入其他屬性(如 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: Counterthis 會被推斷為 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 再做型別斷言。
函式重載實作不完整 實作只符合部分重載簽名,導致呼叫時仍出錯。 在實作函式最後使用最寬鬆的型別(anyunknown)作參數,並在函式內部自行檢查。
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)
    使用 ComposePipe 等高階函式時,需要明確描述每個函式的輸入與輸出型別,才能在編譯階段捕捉組合錯誤。

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


總結

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

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

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