Hello
← 博客

TypeScript 类型体操入门:从 Utility Types 到自定义类型推导

TypeScript 的类型系统是图灵完备的,这意味着你可以在类型层面做几乎任何计算。虽然日常开发不需要过于复杂的类型体操,但掌握核心技巧能显著提升代码的类型安全性。

基础:内置 Utility Types

TypeScript 内置了很多实用类型,先确保你熟练使用它们:

// Partial —— 所有属性变为可选
type Draft = Partial<Article>

// Required —— 所有属性变为必需
type Complete = Required<Draft>

// Pick —— 选取部分属性
type Summary = Pick<Article, 'title' | 'date'>

// Omit —— 排除部分属性
type CreateInput = Omit<Article, 'id' | 'createdAt'>

// Record —— 构建键值对类型
type StatusMap = Record<'active' | 'inactive', User[]>

条件类型

条件类型是类型体操的核心,语法类似三元表达式:

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

type A = IsString<'hello'>  // true
type B = IsString<42>       // false

结合分布式条件类型,可以对联合类型的每个成员独立运算:

type NonNullable<T> = T extends null | undefined ? never : T

type C = NonNullable<string | null | undefined>  // string

infer 关键字

infer 用于在条件类型中提取(推导)某个位置的类型:

// 提取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// 提取 Promise 内部类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never

infer 的强大之处在于它能在模式匹配中「捕获」类型变量。

模板字面量类型

TypeScript 4.1 引入的模板字面量类型让字符串级别的类型操作成为可能:

type EventName<T extends string> = `on${Capitalize<T>}`

type ClickEvent = EventName<'click'>  // 'onClick'
type FocusEvent = EventName<'focus'>  // 'onFocus'

结合映射类型,可以批量生成事件处理器类型:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person { name: string; age: number }
type PersonGetters = Getters<Person>
// { getName: () => string; getAge: () => number }

递归类型

处理嵌套结构时,递归类型不可或缺:

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

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

实战:类型安全的事件系统

综合运用以上技巧,实现一个类型安全的事件总线:

type EventMap = {
  userLogin: { userId: string; timestamp: number }
  pageView: { path: string; referrer?: string }
  error: { code: number; message: string }
}

type EventHandler<T> = (payload: T) => void

class TypedEventBus<Events extends Record<string, any>> {
  private handlers = new Map<string, Set<Function>>()

  on<K extends keyof Events>(
    event: K,
    handler: EventHandler<Events[K]>
  ) {
    if (!this.handlers.has(event as string)) {
      this.handlers.set(event as string, new Set())
    }
    this.handlers.get(event as string)!.add(handler)
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]) {
    this.handlers.get(event as string)?.forEach(h => h(payload))
  }
}

const bus = new TypedEventBus<EventMap>()
bus.on('userLogin', ({ userId }) => console.log(userId))  // 类型正确
bus.emit('error', { code: 500, message: 'fail' })         // 类型正确
bus.emit('error', { wrong: true })                        // 编译报错

建议

类型体操虽然强大,但要克制使用。简单的 as 断言有时比复杂的类型推导更务实。团队中如果只有你能看懂这些类型,那可能就不该用。好的类型设计应该是让使用者感觉自然、IDE 提示清晰,而不是让维护者头疼。