条件类型是 TypeScript 类型系统的"if-else"。


条件类型基础

type IsString<T> = T extends string ? 'yes' : 'no'

type A = IsString<string> // 'yes'
type B = IsString<number> // 'no'

条件类型的语法:T extends U ? X : Y

  • 如果 T 可以赋值给 U,类型是 X
  • 否则,类型是 Y

infer:从已有类型结构中提取信息

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type A = ReturnType<() => string> // string
type B = ReturnType<(x: number) => boolean> // boolean

infer R 表示"推断出函数返回类型,命名为 R"。

这不是为了炫技,而是为了从已有的类型结构中提取信息。比如:

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

type A = Awaited<Promise<string>> // string
type B = Awaited<Promise<Promise<number>>> // number

Discriminated Unions:用类型表达状态机

这才是条件类型真正重要的应用。

// ❌ 不好的设计:多个布尔值
type State = {
  isLoading: boolean
  isError: boolean
  data?: User
  error?: Error
}
// 问题:isLoading 和 isError 可以同时为 true,不可能的状态

// ✅ 好的设计:discriminated union
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: Error }

status 是判别属性(discriminant),它的值决定了其他属性是否存在。


收窄与不可能状态

function UserProfile({ state }: { state: State }) {
  switch (state.status) {
    case 'idle':
      return <div>请输入查询</div>
    case 'loading':
      return <div>加载中...</div>
    case 'success':
      return <div>{state.data.name}</div>  // ✅ TypeScript 知道这里有 data
    case 'error':
      return <div>{state.error.message}</div>  // ✅ TypeScript 知道这里有 error
  }
}

TypeScript 的收窄(narrowing):在 switch 的每个分支里,TypeScript 知道 state 的具体类型。


Exhaustiveness Checking

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`)
}

function UserProfile({ state }: { state: State }) {
  switch (state.status) {
    case 'idle':
      return <div>请输入查询</div>
    case 'loading':
      return <div>加载中...</div>
    case 'success':
      return <div>{state.data.name}</div>
    case 'error':
      return <div>{state.error.message}</div>
    default:
      return assertNever(state)  // ✅ 如果遗漏了某个分支,编译报错
  }
}

如果以后给 State 添加了新分支,但忘记处理,TypeScript 会报错。


核心洞察

TypeScript 不只是描述数据结构,还可以描述系统允许存在的状态空间。

不可能的状态无法表达,编译时就会报错。

这就是类型驱动的状态建模:

用类型约束系统可以处于哪些状态,让非法状态在编译时就被发现。