TypeScript 泛型完全指南

深入 TypeScript 泛型:泛型函式/介面/類別、型別約束、keyof 取屬性、預設型別參數與實戰

接續 TypeScript 基礎篇(同目錄 basics.md);條件型別、工具型別等更進階用法見進階型別篇。


目錄


什麼是泛型?

泛型(Generics) 讓你寫出「型別可重用」的程式:函式或型別先不指定具體型別,留一個型別參數,呼叫時才代入——既保有彈性,又保留型別安全

為什麼需要泛型?

考慮一個「回傳傳入值」的函式:

// ❌ 用 any:失去型別,回傳變 any
function identity(x: any): any { return x; }
const a = identity("hi");   // a 是 any(沒型別保護)

// ❌ 寫死型別:不能重用
function identityStr(x: string): string { return x; }

// ✅ 泛型:彈性 + 型別安全
function identity<T>(x: T): T { return x; }
const b = identity("hi");    // b 推論為 string
const c = identity(123);     // c 推論為 number

<T>型別參數(慣例用 TUKV),它在呼叫時由實際引數推論或明確指定。

核心價值:any 是放棄型別;用泛型是把型別當參數傳遞,兩者天差地別。


泛型函式

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

const n = first([1, 2, 3]);       // number | undefined(自動推論)
const s = first(["a", "b"]);      // string | undefined

// 也可明確指定型別參數
const x = first<boolean>([true]); // 明確指定 T = boolean
  • 型別參數通常能自動推論,不必每次手動寫 <...>
  • 只有推不出或想更精確時才明確指定

泛型介面與型別別名

型別本身也能泛型化,常用於容器、包裝結構:

// 泛型介面
interface Box<T> {
  value: T;
}
const numBox: Box<number> = { value: 42 };

// 泛型型別別名
type Pair<K, V> = { key: K; value: V };
const p: Pair<string, number> = { key: "age", value: 30 };

// 泛型函式型別
type Mapper<T, U> = (item: T) => U;
const toLength: Mapper<string, number> = (s) => s.length;

泛型類別

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 s = new Stack<number>();
s.push(1);
const top = s.pop();   // number | undefined

泛型類別讓同一份資料結構(Stack、Queue、LinkedList…)能安全地裝任何型別。


泛型約束(Constraints)

預設型別參數可以是「任何型別」,但有時需要它至少具備某些性質——用 extends 約束:

// ❌ 沒約束:T 可能沒有 .length
function longest<T>(a: T, b: T): T {
  return a.length > b.length ? a : b;   // 錯:T 不保證有 length
}

// ✅ 約束 T 必須有 length 屬性
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length > b.length ? a : b;
}

longest("ab", "abc");        // ✅ string 有 length
longest([1], [1, 2]);        // ✅ array 有 length
longest(1, 2);               // ❌ number 沒有 length

T extends X 的意思是「T 必須是 X 的子型別(至少滿足 X)」,不是繼承類別。


搭配 keyof 取屬性

泛型 + keyof 是經典組合,做出型別安全的屬性存取

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice" };
const name = getProp(user, "name");   // 推論為 string
const id = getProp(user, "id");       // 推論為 number
getProp(user, "email");               // ❌ "email" 不是 user 的 key
  • keyof T = T 所有屬性名組成的 union(這裡是 "id" | "name"
  • K extends keyof T 約束 key 必須是 T 真實存在的屬性
  • T[K]索引存取型別:取出該屬性的型別

預設型別參數

型別參數可給預設值,呼叫時不指定就用預設:

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

const r1: ApiResponse = { data: "x", status: 200 };          // T = unknown
const r2: ApiResponse<number> = { data: 1, status: 200 };    // T = number

多個型別參數

function zip<T, U>(a: T[], b: U[]): [T, U][] {
  return a.map((item, i) => [item, b[i]]);
}

const pairs = zip(["a", "b"], [1, 2]);   // [string, number][]

慣例命名:T(Type)、K(Key)、V(Value)、E(Element)、R(Return)。但語意清楚時,用有意義的名字(如 TData)更好讀。


實戰範例

1. API 回應包裝

interface Result<T> {
  success: boolean;
  data: T;
  error?: string;
}

async function fetchUser(id: number): Promise<Result<User>> {
  // ...
  return { success: true, data: user };
}

2. 型別安全的事件系統

type EventMap = {
  click: { x: number; y: number };
  message: { text: string };
};

function emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
  // event 與 payload 型別自動對應
}

emit("click", { x: 1, y: 2 });       // ✅
emit("message", { x: 1 });           // ❌ payload 型別不符

3. 約束 + 預設值組合

function createMap<K extends string | number, V = string>(): Map<K, V> {
  return new Map<K, V>();
}

常見陷阱

1. 把泛型誤當成 any

// ❌ 這沒有真正利用泛型——回傳沒關聯到參數
function bad<T>(x: T): any { return x; }

// ✅ 型別參數要貫穿輸入到輸出
function good<T>(x: T): T { return x; }

2. 過度泛型化

不是所有東西都該泛型。只在型別真的需要重用或關聯時才用;單一具體型別就夠時,泛型只是增加複雜度。

3. 約束太鬆或太緊

約束(extends)要剛好夠用:太鬆會在函式內無法安全操作;太緊會限制可用的引數。


常見問題

問題 1:泛型和 any 差在哪?

any 完全放棄型別;泛型是「把型別當參數傳遞」——呼叫時代入具體型別,輸入到輸出的型別關係被保留,型別安全不丟失。

問題 2:T extends X 是繼承嗎?

不是類別繼承。它是型別約束:「T 必須是 X 的子型別 / 至少滿足 X 的形狀」。用來保證在泛型內可以安全使用 X 的性質。

問題 3:什麼時候該用泛型?

當函式/型別要對多種型別重用,且輸入與輸出(或多個參數)之間有型別關聯時。若只是單一固定型別,直接寫具體型別更簡單。

問題 4:型別參數一定要寫 <T> 嗎?

呼叫時多半能自動推論,不必手寫。只有推不出(如沒有引數可推)或想覆寫推論結果時才明確指定。

問題 5:keyof 和泛型怎麼搭?

K extends keyof T 約束 key 必須是物件 T 真實存在的屬性,配合索引存取型別 T[K] 取出對應屬性的型別——做出型別安全的屬性存取/事件系統。


總結

核心要點

  • 泛型 = 把型別當參數:彈性重用 + 保留型別安全(與 any 相反)
  • 函式、介面、型別別名、類別都能泛型化,型別參數多半可自動推論
  • 約束 T extends X:要求型別參數至少滿足某形狀(非類別繼承)
  • K extends keyof T + T[K]:型別安全的屬性存取,經典組合
  • 可給預設型別參數、用多個型別參數
  • 別把泛型寫成 any(型別要貫穿輸入輸出)、別過度泛型化

快速參考

語法 意義
<T> 宣告型別參數
T extends X 約束:T 須滿足 X
keyof T T 的屬性名 union
T[K] 索引存取:取屬性型別
<T = D> 預設型別參數

建立日期:2026-06-18

🔗相關文章