跳至主要內容

TypeScript 條件型別深入:infer 與型別體操實戰

11 分鐘閱讀 1,150 字

TypeScript 條件型別深入:infer 與型別體操實戰

條件型別(Conditional Types)是 TypeScript 型別系統中最強大也最複雜的特性之一。搭配 infer 關鍵字,你可以在型別層面做出邏輯判斷、提取複雜型別的內部結構,寫出極具表達力的型別定義。

條件型別基礎

條件型別的語法類似三元運算子:

type IsString<T> = T extends string ? true : false

type A = IsString<string>   // true
type B = IsString<number>   // false
type C = IsString<'hello'>  // true('hello' extends string)

分佈式條件型別

當條件型別的 T 是裸型別參數(naked type parameter)且傳入 Union 型別時,條件型別會分佈到每個成員:

type ToArray<T> = T extends any ? T[] : never

// 分佈式行為:
type A = ToArray<string | number>
// 等同於:ToArray<string> | ToArray<number>
// 結果:string[] | number[]

// 關閉分佈式行為(用 tuple 包裹):
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never
type B = ToArrayNonDistributive<string | number>
// 結果:(string | number)[]

infer:從型別中提取資訊

infer 是條件型別中的型別推斷關鍵字,讓你能在條件成立時「捕獲」型別:

// 提取函式返回值型別(標準庫的 ReturnType 實作)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type A = MyReturnType<() => string>         // string
type B = MyReturnType<(x: number) => void>  // void
type C = MyReturnType<string>               // never

// 提取函式參數型別
type MyParameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never

type D = MyParameters<(a: string, b: number) => void>  // [string, number]

// 提取 Promise 內部型別
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T  // 遞迴展開

type E = Awaited<Promise<Promise<string>>>  // string

實用型別工具實作

深層 Partial

標準 Partial<T> 只展開一層,深層版本需要遞迴條件型別:

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T

interface Config {
  server: {
    host: string
    port: number
    ssl: {
      enabled: boolean
      cert: string
    }
  }
  database: {
    url: string
  }
}

type PartialConfig = DeepPartial<Config>
// server.ssl.enabled 現在是 boolean | undefined

提取物件中特定型別的鍵

// 取得值為指定型別的鍵
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never
}[keyof T]

interface User {
  id: number
  name: string
  email: string
  age: number
  isAdmin: boolean
}

type StringKeys = KeysOfType<User, string>   // 'name' | 'email'
type NumberKeys = KeysOfType<User, number>   // 'id' | 'age'

函式多載型別提取

// 提取最後一個多載的返回型別
type LastOverload<T> = T extends {
  (...args: any[]): infer R1;
  (...args: any[]): infer R2;
  (...args: any[]): infer R3;
} ? R3 : T extends {
  (...args: any[]): infer R1;
  (...args: any[]): infer R2;
} ? R2 : T extends (...args: any[]) => infer R ? R : never

模板字面量型別(Template Literal Types)

條件型別配合模板字面量,可以做字串型別轉換:

// CamelCase 轉 snake_case
type CamelToSnake<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Head extends Uppercase<Head>
      ? `_${Lowercase<Head>}${CamelToSnake<Tail>}`
      : `${Head}${CamelToSnake<Tail>}`
    : S

type A = CamelToSnake<'helloWorld'>      // 'hello_world'
type B = CamelToSnake<'getUserById'>     // 'get_user_by_id'

// 取得物件所有路徑
type Paths<T, Prefix extends string = ''> = {
  [K in keyof T & string]:
    T[K] extends object
      ? Paths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
      : `${Prefix}${K}`
}[keyof T & string]

interface Settings {
  theme: { mode: string; color: string }
  language: string
}

type SettingPaths = Paths<Settings>
// 'theme' | 'theme.mode' | 'theme.color' | 'language'

實戰:型別安全的事件系統

// 定義事件映射
interface AppEvents {
  'user:login': { userId: string; timestamp: Date }
  'user:logout': { userId: string }
  'cart:add': { productId: string; quantity: number }
  'cart:checkout': { total: number; items: string[] }
}

// 提取事件名和對應的 payload 型別
type EventName = keyof AppEvents
type EventPayload<T extends EventName> = AppEvents[T]

// 型別安全的事件發射器
class EventBus {
  private handlers = new Map<string, Set<Function>>()

  on<E extends EventName>(
    event: E,
    handler: (payload: EventPayload<E>) => void
  ): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set())
    }
    this.handlers.get(event)!.add(handler)
    return () => this.handlers.get(event)?.delete(handler)
  }

  emit<E extends EventName>(event: E, payload: EventPayload<E>): void {
    this.handlers.get(event)?.forEach(h => h(payload))
  }
}

const bus = new EventBus()

// 完整的型別推斷
bus.on('user:login', ({ userId, timestamp }) => {
  console.log(userId, timestamp)  // userId: string, timestamp: Date
})

bus.emit('cart:add', { productId: 'abc', quantity: 2 })  // ✅
bus.emit('cart:add', { productId: 'abc' })               // ❌ 缺少 quantity

型別體操的實際價值

條件型別在以下場景特別有價值:

  1. 框架開發:Vue、React 等框架大量使用條件型別推斷 Props 型別
  2. ORM 型別:Prisma、Drizzle 用條件型別讓查詢結果有精確的型別
  3. API 客戶端:根據請求型別推斷回應型別
  4. 表單驗證庫:Zod、Yup 用條件型別從 Schema 推斷資料型別

總結

條件型別和 infer 是 TypeScript 型別程式設計的核心工具。從基本的 T extends U ? X : Y,到分佈式行為、infer 提取、遞迴型別和模板字面量組合,掌握這些技巧能讓你應對各種複雜的型別需求,也有助於閱讀主流框架的型別定義。

分享這篇文章