TypeScript 泛型与类型推导:从 util type 到条件类型实战
引言
TypeScript 的类型系统是图灵完备的——这意味着你可以在类型层面做"计算"。从基础的泛型到条件类型、infer、mapped types、template literal types,掌握这些工具是编写类型安全库的前提。本文将系统性地讲解每一个核心概念,并配实战案例。
一、泛型基础与约束
1.1 泛型函数
function identity<T>(value: T): T {
return value
}
const s = identity('hello') // T = string
const n = identity(42) // T = number
泛型本质:类型的参数化。调用时由参数推导,或显式指定。
1.2 多参数泛型与默认值
function createState<T, U = T[]>(
initial: U
): { value: U; push: (item: T) => void } {
let value = initial
return {
get value() { return value },
push: (item) => { value = [...value, item] as U }
}
}
const s = createState<string>([]) // U 推导为 string[]
const n = createState<number, number>(0) // 显式指定
1.3 泛型约束(extends)
// 约束 T 必须有 length 属性
function getLength<T extends { length: number }>(arg: T): number {
return arg.length
}
getLength('hello') // ✅ string.length
getLength([1, 2, 3]) // ✅ array.length
// getLength(42) // ❌ number 没有 length
// keyof 约束
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
1.4 NoInfer(TS 5.4+)
function createGroup<T>(items: T[], defaultItem: NoInfer<T>): T[] {
return [...items, defaultItem]
}
createGroup([1, 2, 3], 0) // T = number
// createGroup([1, 2, 3], '0') // ❌ NoInfer 让 defaultItem 不参与推导
NoInfer 的用途:阻止某些参数参与类型推导,让泛型的推导结果只来自"主要"参数。
二、协变与逆变
| 概念 | 含义 | 示例 |
|---|---|---|
| 协变 | T 兼容 U ⟹ F<T> 兼容 F<U> |
数组、Promise、返回值 |
| 逆变 | T 兼容 U ⟹ F<U> 兼容 F<T> |
函数参数 |
| 双变 | 两者皆可(不安全) | strictFunctionTypes: false |
| 不变 | 都不兼容 | 泛型类实例 |
class Animal { name: string = '' }
class Dog extends Animal { bark() {} }
// 数组是协变的
let animals: Animal[] = [new Dog()] // ✅ Dog[] 兼容 Animal[]
// 函数参数是逆变的(strictFunctionTypes: true)
type Handler<T> = (arg: T) => void
declare let dogHandler: Handler<Dog>
declare let animalHandler: Handler<Animal>
dogHandler = animalHandler // ✅ 逆变
// animalHandler = dogHandler // ❌
三、条件类型与分布式条件类型
3.1 基础语法
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
type C = IsString<string | number> // true | false → boolean
3.2 分布式条件类型
当 T 是裸类型参数且传入联合类型时,条件类型会分发到每个成员:
type ToArray<T> = T extends any ? T[] : never
type R = ToArray<string | number>
// 等价于 ToArray<string> | ToArray<number>
// = string[] | number[]
阻止分发:用方括号包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type R2 = ToArrayNonDist<string | number>
// = (string | number)[]
3.3 实战:排除类型成员
type NonNullable<T> = T extends null | undefined ? never : T
type R = NonNullable<string | null | number | undefined> // string | number
type ExcludeFunctions<T> = T extends Function ? never : T
type R2 = ExcludeFunctions<string | (() => void) | number> // string | number
3.4 递归解包 Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T
type A = UnwrapPromise<Promise<string>> // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T
type D = DeepUnwrap<Promise<Promise<number>>> // number
四、infer 关键字
infer 在条件类型的 extends 子句中声明待推导的类型变量。
4.1 函数返回值与参数
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R = ReturnType<() => string> // string
type FirstParameter<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never
type R2 = FirstParameter<(name: string, age: number) => void> // string
4.2 数组元素与 Tuple
type ElementOf<T> = T extends (infer E)[] ? E : never
type R = ElementOf<string[]> // string
type Second<T extends any[]> = T extends [any, infer S, ...any[]] ? S : never
type R2 = Second<[string, number, boolean]> // number
4.3 字符串模板拆分
type FirstWord<S> = S extends `${infer Head} ${string}` ? Head : S
type R = FirstWord<'hello world'> // 'hello'
4.4 Promise.all 类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type PromiseAll<T extends readonly Promise<any>[]> = Promise<{
[K in keyof T]: Awaited<T[K]>
}>
const r = promiseAll([Promise.resolve(1), Promise.resolve('hello'), Promise.resolve(true)] as const)
// 类型:Promise<readonly [number, string, boolean]>
五、Mapped Types
5.1 基础映射
type Readonly<T> = { readonly [K in keyof T]: T[K] }
type Optional<T> = { [K in keyof T]?: T[K] }
type Required<T> = { [K in keyof T]-?: T[K] }
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
5.2 Key Remapping via as
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }
type StringFields<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
type R = StringFields<{ name: string; age: number; email: string }>
// { name: string; email: string }
六、Template Literal Types
6.1 基础
type Greeting = `hello ${string}`
const g: Greeting = 'hello world' // ✅
type Side = 'top' | 'right' | 'bottom' | 'left'
type Margin = `margin-${Side}`
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'
6.2 内置 String Manipulation
type Upper = Uppercase<'hello'> // 'HELLO'
type Lower = Lowercase<'HELLO'> // 'hello'
type Cap = Capitalize<'hello'> // 'Hello'
type Uncap = Uncapitalize<'Hello'> // 'hello'
6.3 实战:路由参数类型化
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<`/${Rest}`>
: T extends `${infer _Start}:${infer Param}`
? { [K in Param]: string }
: {}
type Params = ExtractParams<'/users/:userId/posts/:postId'>
// { userId: string; postId: string }
function route<T extends string>(path: T, params: ExtractParams<T>): string {
let result = path as string
for (const key in params) result = result.replace(`:${key}`, (params as any)[key])
return result
}
route('/users/:userId/posts/:postId', { userId: '1', postId: '2' }) // ✅
// route('/users/:userId', { postId: '2' }) // ❌ 缺少 userId
七、内置 Utility Types 源码解析
type Partial_2<T> = { [K in keyof T]?: T[K] }
type Required_2<T> = { [K in keyof T]-?: T[K] }
type Pick_2<T, K extends keyof T> = { [P in K]: T[P] }
type Omit_2<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
type Record_2<K extends keyof any, V> = { [P in K]: V }
type Parameters_2<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never
type ReturnType_2<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never
八、类型体操实战
8.1 DeepPartial 与 DeepReadonly
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function ? T[K] : DeepPartial<T[K]>
: T[K]
}
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function ? T[K] : DeepReadonly<T[K]>
: T[K]
}
8.2 Path 路径类型生成
type Path<T, P extends string = ''> = T extends object
? {
[K in keyof T & string]:
T[K] extends object
? Path<T[K], `${P}${P extends '' ? '' : '.'}${K}`>
: `${P}${P extends '' ? '' : '.'}${K}`
}[keyof T & string]
: never
type DataPath = Path<{ user: { name: string; age: number }; settings: { theme: string } }>
// 'user.name' | 'user.age' | 'settings.theme'
8.3 带类型的 EventEmitter
class TypedEmitter<Events extends Record<string, any[]>> {
private handlers: { [K in keyof Events]?: Array<(...args: Events[K]) => void> } = {}
on<K extends keyof Events>(event: K, handler: (...args: Events[K]) => void): this {
(this.handlers[event] ??= []).push(handler)
return this
}
emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
this.handlers[event]?.forEach(h => h(...args))
}
}
interface ServerEvents {
connection: [socketId: string]
data: [chunk: Buffer, encoding: string]
error: [err: Error]
}
const emitter = new TypedEmitter<ServerEvents>()
emitter.on('connection', (id) => { /* id: string ✅ */ })
emitter.emit('data', Buffer.from('x'), 'utf8') // ✅
// emitter.emit('data', 'wrong') // ❌ 参数数量不对
// emitter.on('foo', () => {}) // ❌ 事件不存在
九、总结
| 能力 | 工具 | 适用场景 |
|---|---|---|
| 参数化 | 泛型 | 可复用函数/类 |
| 类型判断 | 条件类型 | 根据输入决定输出类型 |
| 类型推导 | infer | 从已有结构提取内部类型 |
| 结构变换 | mapped types | 批量修改属性 |
| 字符串操作 | template literal types | 路由、CSS 属性、API 名 |
| 联合操作 | 分布式条件类型 | 过滤、转换联合成员 |
类型体操不是炫技:正确的类型表达能将运行时错误提前到编译时,是构建高质量库的基础。当你能用类型表达"函数名 === API 路由路径"这种约束时,才算真正掌握了 TypeScript。
Comments | 0条评论