日常程式碼中的 TypeScript 泛型

Frontend2026-10-05TryQuickToolBox

你正在撰寫一個回傳陣列第一個元素的函式。你希望它能適用於數字、字串和物件,但同時也希望回傳型別能符合輸入型別。如果不使用泛型,你只能求助於 any,並失去所有型別安全。這就是 TypeScript 泛型派上用場的地方。

泛型讓你能撰寫適用於多種型別的程式碼,同時保留具體的型別資訊。在本指南中,我們將探討在日常 TypeScript 程式碼中使用泛型的實用模式。

什麼是泛型?

泛型是一種將型別參數化的方式。就像函式接受參數一樣,泛型接受型別參數。它們允許你建立可重複使用的元件,適用於各種型別,而不是單一型別。

考慮這個 identity 函式:

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

在這裡,T 是一個型別變數。當你呼叫 identity(42) 時,TypeScript 會推斷 T 為 number。當你呼叫 identity("hello") 時,T 會變成 string。回傳型別總是符合輸入型別。

實務中的泛型函式

讓我們來看一個常見情境:一個將值包裝在物件中的函式。

function wrapInObject<T>(value: T): { value: T } {
  return { value };
}

如果不使用泛型,你必須使用 any 或 unknown,而呼叫端會失去型別資訊。使用泛型時,回傳型別精確地是 { value: T }。

另一個日常例子是回傳陣列最後一個元素的函式:

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

TypeScript 會從陣列參數推斷 T,所以 last([1, 2, 3]) 會回傳 number | undefined。

泛型約束

有時你想限制可與泛型一起使用的型別。例如,你可能需要存取只存在於特定型別上的屬性。你可以使用 extends 關鍵字來新增約束。

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

現在 T 必須有一個型別為 number 的 length 屬性。這適用於字串、陣列,以及任何具有 length 屬性的物件。

你也可以將約束與介面一起使用:

interface HasId {
  id: string;
}

function findById<T extends HasId>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id);
}

這確保傳遞給 findById 的項目具有 id 屬性。

泛型介面與型別別名

泛型不僅限於函式。你可以定義泛型介面和型別別名來建立靈活的資料結構。

interface ApiResponse<T> {
  data: T;
  status: number;
  error?: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { data, status: response.status };
}

現在你可以精確地為 API 回應加上型別:

interface User {
  id: number;
  name: string;
}

const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User

泛型類別

類別也可以是泛型的。一個常見例子是簡單的堆疊:

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

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

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

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined

React 元件中的泛型

如果你正在建置 React 元件,泛型可以讓你的元件更具可重複使用性。例如,一個渲染任何型別項目的列表元件:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}

現在你可以將 List 與任何型別一起使用,並在 renderItem 回呼上獲得完整的型別檢查。

常見陷阱與最佳實踐

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

方法 型別安全 靈活性 使用情境
any 無 高 遷移 JS、快速原型
聯合型別 良好 限於已知型別 當你知道所有可能的型別時
泛型 強 高 可重複使用且保留型別的元件

常見問題

何時應該使用泛型而不是 any?

每當你想在函式或元件中保留型別資訊時,就使用泛型。any 會停用型別檢查,而泛型會保留它。如果你發現自己為了讓某些功能運作而求助於 any,請考慮泛型是否能更安全地解決問題。

我可以使用多個型別參數嗎?

可以。你可以定義具有多個型別參數的函式,例如 function pair<K, V>(key: K, value: V): [K, V]。當有多個參數時,請使用描述性的名稱。

泛型在 .tsx 檔案中如何與箭頭函式搭配使用?

在 .tsx 檔案中,<T> 可能與 JSX 產生歧義。請使用尾隨逗號:const identity = <T,>(arg: T): T => arg; 或使用 extends unknown:<T extends unknown>。

泛型是一項強大的功能,可以讓你的 TypeScript 程式碼更具可重複使用性和型別安全。從簡單的泛型函式開始,然後逐漸將它們應用到介面和類別。如果你需要以型別安全的方式格式化或驗證 JSON 資料,請查看我們的 JSON Formatter 工具,快速檢查並美化你的 JSON。