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型別體操的實際價值
條件型別在以下場景特別有價值:
- 框架開發:Vue、React 等框架大量使用條件型別推斷 Props 型別
- ORM 型別:Prisma、Drizzle 用條件型別讓查詢結果有精確的型別
- API 客戶端:根據請求型別推斷回應型別
- 表單驗證庫:Zod、Yup 用條件型別從 Schema 推斷資料型別
總結
條件型別和 infer 是 TypeScript 型別程式設計的核心工具。從基本的 T extends U ? X : Y,到分佈式行為、infer 提取、遞迴型別和模板字面量組合,掌握這些技巧能讓你應對各種複雜的型別需求,也有助於閱讀主流框架的型別定義。
分享這篇文章