类型体操入门实践
一句话概括
类型体操 = 用 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/else,infer 当解构,[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 的 selector:
useStore(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、递归=函数自调用——剩下的就是多写多练。