TypeScript 泛型日常實用指南:寫出靈活又型別安全的程式碼

Frontend2026-09-21TryQuickToolBox

你正在撰寫一個可處理任何型別的函式,但不想失去型別安全。你可以使用 any,但這就違背了 TypeScript 的初衷。泛型讓你撰寫可處理多種型別的程式碼,同時保留型別資訊。本指南將展示如何在日常程式碼中使用泛型,而不會把事情搞得太複雜。

什麼是泛型?

泛型是一種建立可重用元件的方式,讓元件能適用於各種型別,而非僅限單一型別。它們讓你捕捉輸入的型別並用於輸出,在確保型別安全的同時保有靈活性。

把泛型想成型別參數。就像函式接受值參數一樣,泛型函式接受型別參數。例如 identity 函式:

function identity<T>(arg: T): T {
  return arg;
}

在這裡,T 是型別參數。當你呼叫 identity<string>("hello") 時,TypeScript 會推斷 T 為 string,回傳型別即為 string。若傳入數字,則回傳數字。這比使用 any 更精確。

為什麼不直接用 any?

使用 any 會停用型別檢查。你將失去自動補全、重構支援,編譯器也無法捕捉錯誤。泛型讓型別資訊在程式碼中持續流動,因此你能同時獲得靈活性與安全性。

實務中的泛型函式

來看一個常見情境:回傳陣列第一個元素的函式。

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

沒有泛型時,你可能會寫成 function firstElement(arr: any[]): any,這就丟失了型別。有了泛型,若傳入 string[],回傳型別就是 string | undefined。

你也可以使用多個型別參數。例如,使用回呼將一個陣列映射到新陣列的函式:

function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

這基本上就是 TypeScript 標準函式庫中 Array.prototype.map 的型別定義方式。

泛型介面與型別別名

泛型不限於函式。你可以建立泛型介面與型別別名,定義可重用的資料結構。

interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };

這對 API 回應、狀態管理或任何容器型別都很有用。

你也可以提供預設型別:

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

現在 ApiResponse 在沒有型別引數時會預設為 unknown。

泛型約束

有時你想限制泛型可使用的型別。你可以使用 extends 關鍵字來加入約束。

function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

現在 T 必須具有 length 屬性。你可以呼叫 getLength("hello") 或 getLength([1,2,3]),但不能呼叫 getLength(42)。

當處理具有共同形狀的物件時,約束非常強大。

在類別中使用泛型

類別也可以是泛型的。常見例子是泛型集合:

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined

這確保只有數字能被推入數字堆疊。

實戰範例:型別安全的事件發送器

讓我們建立一個簡單的事件發送器,使用泛型將事件名稱對應到 payload 型別。

type EventMap = Record<string, any>;

class EventEmitter<T extends EventMap> {
  private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.listeners[event]?.forEach(listener => listener(payload));
  }
}

// 使用方式
interface MyEvents {
  login: { userId: string };
  logout: { userId: string };
}

const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
  console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Error

這個模式為事件名稱與 payload 提供完整的型別安全,減少執行時期錯誤。

常見陷阱與最佳實踐

比較:泛型 vs. any vs. 聯合型別

做法 型別安全 靈活性 使用情境
泛型 高 高 保留型別的可重用元件
any 無 高 快速原型開發、移轉 JS
聯合型別 中 中 已知型別的有限集合

泛型在靈活性與安全性之間取得平衡,讓它們成為函式庫與共用工具的理想選擇。

常見問題

什麼時候該用泛型而不是聯合型別?

當輸入與輸出之間的型別關係很重要時,請使用泛型。聯合型別適用於值可以是固定幾種型別之一,但該特定型別不需要在操作之間被保留的情況。

泛型可以搭配箭頭函式使用嗎?

可以,但在 .tsx 檔案中語法可能較為棘手。你可以寫成 const identity = <T,>(arg: T): T => arg;,加上尾隨逗號以避免與 JSX 產生歧義。

如何將泛型約束為只接受物件?

使用 T extends object 或 T extends Record<string, unknown> 來確保 T 是物件型別。

泛型是 TypeScript 型別系統的基石。在日常程式碼中使用它們,你就能寫出既靈活又安全的函式、介面與類別。從簡單的泛型函式開始,再視需要逐步加入約束與更複雜的模式。想了解更多開發者工具,歡迎參考我們的 JSON Formatter,快速驗證並格式化你的 JSON 資料。