文章

类型体操入门实践

类型体操入门实践

一句话概括

类型体操 = 用 TypeScript 的类型系统写”程序”——利用条件类型、映射类型、infer、递归类型等特性,在编译期完成类型推导和校验。它决定了你是”会用 TS”还是”精通 TS”,也是大厂面试的高频深水区。

核心知识点

1. 分布式条件类型——拆开联合,逐个判断

1
2
3
4
5
6
7
8
9
// 裸类型参数触发分发
type MyExclude<T, U> = T extends U ? never : T;

// 'a' | 'b' | 'c' 被拆成三份分别判断
type R = MyExclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'

// 想关闭分发?用 [] 包裹
type NoDistribute<T> = [T] extends [string] ? 'yes' : 'no';
type R2 = NoDistribute<'a' | 1>; // 'no'(不分发,整个联合一起判断)

面试必备:能解释清楚为什么 Exclude<never, string> 返回 never —— 因为 never 是空联合,分布式条件类型没有成员可供分发,直接短路返回 never

2. infer——类型层面的解构赋值

1
2
3
4
5
6
7
8
9
10
// 提取 Promise 内部值
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type R = Awaited<Promise<Promise<number>>>; // number

// 提取函数参数/返回值
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

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

3. 映射类型 + as 子句——键变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 给所有 key 加 get 前缀
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

// 按类型过滤 key
type PickByType<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};
type StringProps = PickByType<User, string>; // { name: string }

4. 递归类型——类型系统里的 while 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 深层可选
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function ? T[K] : DeepPartial<T[K]>
    : T[K];
};

// 数组版 flat
type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]
  ? First extends any[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : [];

type R = Flatten<[1, [2, [3]]]>; // [1, 2, 3]

心法:把 extends ? :if/elseinfer 当解构,[K in keyof T]map,递归类型当递归函数——几乎就是 JS 代码的类型层映射。

5. 联合转交叉——类型体操的”经典套路”

1
2
3
4
5
6
7
// 利用函数参数逆变
type UnionToIntersection<U> =
  (U extends any ? (arg: U) => void : never) extends
    (arg: infer I) => void ? I : never;

type R = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }

原理:多个函数类型的联合,其参数位置的兼容性是逆变的,TS 推断出的是所有参数的交叉类型。

其实你每天都在用

  • Vue 3 的 defineProps:传进来的 props 被 DeepReadonly 包裹,TS 自动检查你是否在子组件里改了 props
  • axios 响应类型推导axios.get<User[]>('/api') → data 自动是 User[],背后是 infer 提取了泛型参数
  • lodash 的类型定义_.get(obj, 'a.b.c') 能提示路径和返回值类型,靠的是模板字面量类型 + 递归
  • Redux / Zustand 的 selectoruseStore(s => s.user.name) 返回值类型能自动推导,正是 infer + 条件类型的功劳
  • 组件库 props 透传:用 Pick/Omit/Extract 筛选原生 HTML 属性传给底层元素,避免类型打架

常见误解

  • ❌ 误区:「类型体操没有实际价值,就是面试造火箭」 Vue 3、Prisma、trpc、zod 等头部项目的类型定义都在用递归/条件/映射类型。没有这些,开发者体验会倒退到 any 横行的时代。类型体操的价值在于让类型系统替你守住运行时错误。

  • ❌ 误区:「T extends U ? X : Y 里的 T 一定会分发」 不一定。只有 T 是裸类型参数时才会分发。[T]T[]{k: T} 都不会触发分发。这就是为什么 NonNullable<never> 返回 never 而不是 NonNullable——因为 never 是空联合,分发时无成员可处理。

  • ❌ 误区:「递归类型可以无限深层」 TypeScript 对递归类型有深度限制(约 50 层),超出会报 “Type instantiation is excessively deep”。写 DeepReadonly 时一定要留好基本条件(如函数类型直接返回),否则编辑器直接卡死。

  • ❌ 误区:「学类型体操先背内置工具类型的写法就行」 背写法只解决手写题,真正的能力在于理解”何时用条件类型、何时用映射类型、何时要递归”的选型判断。建议从 type-challenges 的 easy 题开始动手,体会每种”动作”的使用场景。

一句话总结

类型体操不可怕,用 JS 思维映射到类型层:条件=if、infer=解构、映射=map、递归=函数自调用——剩下的就是多写多练。

本文由作者按照 CC BY 4.0 进行授权