跳至主要內容

TypeScript 泛型實戰指南:打造可複用的型別安全程式碼

11 分鐘閱讀 1,200 字

TypeScript 泛型實戰指南:打造可複用的型別安全程式碼

泛型(Generics)是 TypeScript 中最強大也最常被誤解的特性之一。許多開發者遇到複雜的泛型語法就跳過,但其實只要掌握核心概念,泛型能讓你寫出既靈活又安全的程式碼。

為什麼需要泛型

考慮一個簡單的函式,取得陣列的第一個元素:

// 沒有泛型:失去型別資訊
function first(arr: any[]): any {
  return arr[0]
}

const num = first([1, 2, 3])  // 型別:any,IDE 無法自動補全

// 使用泛型:保留型別資訊
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

const num = first([1, 2, 3])  // 型別:number
const str = first(['a', 'b'])  // 型別:string

泛型讓函式、類別、介面可以處理多種型別,同時保持完整的型別安全。

泛型約束(Constraints)

使用 extends 限制泛型的範圍:

// 只接受有 length 屬性的型別
function getLength<T extends { length: number }>(value: T): number {
  return value.length
}

getLength("hello")     // ✅ string 有 length
getLength([1, 2, 3])   // ✅ array 有 length
getLength(123)         // ❌ 型別錯誤:number 沒有 length

// 約束為物件的鍵名
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: "Alice", age: 30 }
const name = getProperty(user, "name")  // 型別:string
const age = getProperty(user, "age")    // 型別:number
getProperty(user, "email")              // ❌ 型別錯誤

實用工具型別的實作原理

TypeScript 內建的 PartialRequiredPickOmit 等工具型別都是泛型實作的:

// Partial<T>:所有屬性變為可選
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

// Required<T>:所有屬性變為必填
type MyRequired<T> = {
  [K in keyof T]-?: T[K]  // -? 移除可選標記
}

// Pick<T, K>:只保留指定屬性
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}

// Omit<T, K>:排除指定屬性
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>

// 使用範例
interface User {
  id: number
  name: string
  email: string
  password: string
}

type UserPreview = Pick<User, "id" | "name">  // 只有 id 和 name
type UserUpdate = Partial<Omit<User, "id">>   // 排除 id,其他都可選

泛型函式與多型別參數

// 合併兩個物件,型別安全地
function merge<T extends object, U extends object>(target: T, source: U): T & U {
  return { ...target, ...source }
}

const result = merge(
  { name: "Alice" },
  { age: 30, role: "admin" }
)
// result 的型別:{ name: string } & { age: number; role: string }
console.log(result.name)  // ✅
console.log(result.age)   // ✅

// 型別安全的事件發射器
type EventMap = {
  click: { x: number; y: number }
  keydown: { key: string; code: string }
  resize: { width: number; height: number }
}

class TypedEventEmitter<Events extends Record<string, any>> {
  private listeners: Partial<{
    [K in keyof Events]: Array<(data: Events[K]) => void>
  }> = {}

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

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

const emitter = new TypedEventEmitter<EventMap>()
emitter.on("click", ({ x, y }) => console.log(x, y))  // 型別正確推斷
emitter.emit("click", { x: 10, y: 20 })               // ✅
emitter.emit("click", { key: "a" })                    // ❌ 型別錯誤

實戰:泛型 API 客戶端

interface ApiResponse<T> {
  data: T
  status: number
  message: string
}

interface PaginatedResponse<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
}

async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url)
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
  }
  return response.json()
}

async function fetchPaginated<T>(
  url: string,
  page: number,
  pageSize: number
): Promise<PaginatedResponse<T>> {
  const params = new URLSearchParams({
    page: String(page),
    pageSize: String(pageSize)
  })
  const response = await fetchApi<PaginatedResponse<T>>(`${url}?${params}`)
  return response.data
}

// 使用
interface Post {
  id: number
  title: string
  content: string
}

const posts = await fetchPaginated<Post>("/api/posts", 1, 20)
const firstPost = posts.items[0]  // 型別:Post,完整型別推斷

條件型別入門

// 根據條件選擇不同型別
type IsArray<T> = T extends any[] ? true : false

type A = IsArray<string[]>  // true
type B = IsArray<string>    // false

// 提取 Promise 的內部型別
type Awaited<T> = T extends Promise<infer U> ? U : T

type C = Awaited<Promise<string>>  // string
type D = Awaited<number>           // number

// 從函式型別提取返回值
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never

總結

泛型是 TypeScript 型別系統的核心,從簡單的型別參數到複雜的條件型別,它讓程式碼在保持靈活性的同時不失型別安全。建議從實際需求出發,先掌握基本泛型和約束,再逐步探索映射型別和條件型別。

分享這篇文章