TypeScript 类型体操入门:从 typeof 到 infer

类型体操不是炫技,它解决的是“让代码更难写错”。本文用循序渐进的例子讲清 typeof、keyof、索引访问类型、条件类型、infer 的推断技巧,并给出一套做题思路与常见坑。

18 分钟阅读
小明

TypeScript 类型体操入门:从 typeof 到 infer

你会在 TS 学习路上遇到一个分水岭:

  • 前半段:给变量/函数加类型
  • 后半段:读懂一串像“加密文本”的类型表达式

比如:

type Flatten<T> = T extends (infer U)[] ? U : T

很多人看到 infer 就开始皱眉:

  • 这是在运行时做推断吗?
  • 为什么它能“从类型里拆出类型”?
  • 面试为什么老考?

这篇文章的目标是:

  • 让你理解“类型体操”到底在解决什么
  • 掌握 typeofkeyof → 条件类型 → infer 的核心套路
  • 能自己写出 5~8 个常用工具类型

1. 先定调:类型体操在干什么?

类型体操本质上是:

TypeScript 的类型系统 里写“函数”,把输入类型变换为输出类型。

它发生在编译期,不影响运行时性能。

典型诉求:

  • 从 API 返回类型里提取 data
  • 从函数类型里拿到参数/返回值
  • 把联合类型变成映射对象
  • 让某个字段必填/可选/只读

2. typeof:从值到类型

typeof 在 TS 里最常用于“从一个值拿到它的类型”。

const config = {
  env: 'prod',
  retry: 3,
}

type Config = typeof config
// { env: string; retry: number }

注意:

  • 这是类型层面的 typeof
  • 不是运行时 JS 的 typeof 运算(虽然语法一样)

配合 as const 会更强:

const roles = ['admin', 'user'] as const

type Role = typeof roles[number]
// 'admin' | 'user'

这里的 [number] 是索引访问类型:

  • T[number] 表示数组元素类型

3. keyof:拿到对象的 key 联合

type User = { id: string; name: string }

type UserKeys = keyof User
// 'id' | 'name'

工程里常见用法:做“安全的 key 访问”。


4. 条件类型:类型层面的 if

条件类型长这样:

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

你可以把它当作:

  • 如果 T 可以赋值给 string,则返回 true,否则 false

更实用的例子:提取 Promise 的 resolve 类型:

type AwaitedLike<T> = T extends Promise<infer U> ? U : T

这里就引入了核心主角:infer


5. infer:在条件类型里“声明一个待推断变量”

infer 只能出现在 extends 的右侧,意义是:

让编译器在匹配结构时,把某一部分“抓出来”绑定给一个类型变量。

例子:从数组类型里拿元素类型:

type ElementOf<T> = T extends (infer U)[] ? U : never

type A = ElementOf<number[]>   // number
type B = ElementOf<string[]>   // string

推演:

  • 如果 T 能匹配成 U[]
  • U 就是元素类型

5.1 从函数类型中提取参数与返回值

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

type F = (a: string, b: number) => boolean

type P = Params<F>  // [string, number]
type R = Return<F>  // boolean

你会发现:很多内置工具类型就是这么做的。


6. 分布式条件类型:联合类型会被“逐个处理”

这是类型体操的第二个分水岭。

T 是联合类型,并且 T 直接出现在 extends 左侧时:

type ToArray<T> = T extends any ? T[] : never

type X = ToArray<string | number>
// string[] | number[]

它会对联合里的每个成员分别计算,再把结果 union 回去。

这是“神奇”的来源,也是坑的来源。

如果你不想分布式,可以把 T 包一层:

type NoDistribute<T> = [T] extends [any] ? T : never

7. 做题思路:先画“输入结构”,再写匹配

类型体操不靠灵感,靠套路:

  1. 明确输入类型长什么样(数组?Promise?函数?对象?)
  2. extends 去匹配结构
  3. infer 把关键部分抓出来
  4. 组合成输出

练习题(建议你自己写一遍):

  • First<T>:取元组第一个元素
  • Last<T>:取元组最后一个元素
  • UnwrapPromise<T>:取 Promise resolve 类型
  • DeepReadonly<T>:递归只读

8. 面试如何讲类型体操?

你可以说:

  • TS 类型体操是编译期的类型变换。
  • 主要靠条件类型 + infer 提取结构信息。
  • 工程价值是提升类型表达力,减少运行时错误。

这比“我会写 infer”更像工程师。


总结

  • typeof:从值拿类型
  • keyof:拿 key 联合
  • 条件类型:类型层面的 if
  • infer:从结构里抓出一部分类型
  • 分布式条件类型:联合会逐个处理

如果你愿意,我可以下一篇继续写:

  • 10 个高频工具类型手写(Pick/Omit/ReturnType/Parameters/Exclude/Extract…)
  • 并讲清每个在业务里的落地场景