WSの小屋

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 兼容 UF<T> 兼容 F<U> 数组、Promise、返回值
逆变 T 兼容 UF<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条评论