内置工具类型源码解读
一句话概括
TypeScript 内置工具类型(Partial、Pick、Exclude、ReturnType 等)面试手写率极高。它们的源码加起来不过几十行,核心是映射类型 + 分布式条件类型 + infer 的三件套。
核心知识点
1. Partial / Required / Readonly —— 映射类型修饰符
1
2
3
4
type MyPartial<T> = { [P in keyof T]?: T[P] };
type MyRequired<T> = { [P in keyof T]-?: T[P] }; // -? 去掉可选
type MyReadonly<T> = { readonly [P in keyof T]: T[P] };
type MyMutable<T> = { -readonly [P in keyof T]: T[P] }; // 去掉只读
关键:-? / -readonly 是去掉修饰符;不加 - 是加上修饰符。映射类型天然是同态的(保留原修饰符),所以 Partial 里没写 -readonly,原属性的 readonly 不会被覆盖。
2. Pick / Omit / Record —— 属性的选取与排除
1
2
3
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type MyRecord<K extends keyof any, T> = { [P in K]: T };
面试常问:Record 和 { [key: string]: T } 的区别 —— Record 的键是确定的联合类型字面量,索引签名接受任意 string 键,前者更严格。
3. Exclude / Extract —— 分布式条件类型的明星
1
2
3
4
5
6
7
8
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
// Exclude<'a' | 'b' | 'c', 'a'> 的执行过程:
// 'a' extends 'a' → never
// 'b' extends 'a' → 'b'
// 'c' extends 'a' → 'c'
// 合并:never | 'b' | 'c' → 'b' | 'c'
为什么 never 在联合中会被吞掉?因为 never 是空联合,T | never === T(恒等律)。
4. ReturnType / Parameters —— infer 经典应用
1
2
3
4
5
6
7
8
9
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type Fn = (x: number, y: string) => boolean;
type P = MyParameters<Fn>; // [x: number, y: string]
type R = MyReturnType<Fn>; // boolean
5. NonNullable 与 never 陷阱
1
2
3
4
5
6
type MyNonNullable<T> = T extends null | undefined ? never : T;
// 等价于 Exclude<T, null | undefined>
type T1 = MyNonNullable<string | null>; // string ✅
type T2 = MyNonNullable<never>; // never ⚠️ 面试高频!
// 解释:never 是空联合,分布式条件类型短路直接返回 never
其实你每天都在用
- API 的请求/响应类型抽离:
Pick<User, 'id' | 'name'>只取需要的字段传给前端 - 表单数据 vs 数据库模型:
Omit<User, 'password'>去掉敏感字段再返回 - 组件 Props 透传:
Pick<ButtonHTMLAttributes, 'disabled' | 'type'>精准控制暴露哪些原生属性 - 路由参数提取:
Parameters<typeof router.push>拿到 push 方法的参数类型做二次封装 - Redux action 类型:
ReturnType<typeof createAction>自动推导 action 对象类型
常见误解
❌ 误区:「
Exclude<T, U>里的 T 不一定是联合」 是的,但如果不是联合,extends就不分发。此时Exclude<string, string>的结果是never(因为string extends string走 true 分支返回 never),这和直觉可能不同。❌ 误区:「
Pick能筛选任意条件」 不能。Pick<T, K>只能按键名筛选,不能按值类型筛选。按值类型筛选要用映射类型的as子句,例如[K in keyof T as T[K] extends string ? K : never]。❌ 误区:「
Omit是 TS 内置关键字,性能更好」Omit只是Pick + Exclude的语法糖,和手写没有性能差异。面试官问”手写 Omit”时,不要写成{ [P in Exclude<keyof T, K>]: T[P] }(虽然也对),标准答案是Pick<T, Exclude<keyof T, K>>。❌ 误区:「
ReturnType可以提取任何函数的返回类型」 泛型约束extends (...args: any) => any已经限制了只能是函数。传入非函数类型直接类型报错,不会走到条件判断。此外,重载函数的ReturnType只能拿到最后一个重载签名。
一句话总结
工具类型的源码 = 映射类型做迭代 + 条件类型做判断 + infer 做提取——这三板斧组合好了,没有拆不动的类型。