内置工具类型源码解读深度解析
Partial、Required、Readonly、Pick、Omit、Exclude 等内置工具类型源码加起来才几十行,却是面试手写率最高的部分。 底层全是映射类型加条件类型加 infer 三板斧的组合,拆懂这三招就能手写任意内置工具类型。
一句话概括
TypeScript 内置工具类型的源码加起来才几十行,但面试手写率极高。核心就三板斧:映射类型做遍历、条件类型做判断、infer 做提取。三招组合好了,没有拆不动的类型。
核心知识点
1. 映射三件套:Partial / Required / Readonly
1
2
3
4
5
6
7
8
9
// 记住四个修饰符:?(可选)、readonly(只读)、-?(去可选)、-readonly(去只读)
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] }; // 去掉只读
// 面试常问:为什么 Partial 里没写 -readonly?
// 答:映射类型天然是"同态的"——保留原 key 的所有修饰符(readonly、可选)
// { [P in keyof T]?: T[P] } 生成的属性依然保持原来的 readonly 标记
2. 选取三件套:Pick / Omit / Record
1
2
3
4
5
6
7
8
9
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyRecord<K extends keyof any, T> = { [P in K]: T };
// Omit 的标准答案(不是手写映射,而是组合 Pick + Exclude)
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 💡 Record vs 索引签名区别:
type A = Record<'a' | 'b', string>; // { a: string; b: string }——键确定
type B = { [key: string]: string }; // 任意 string 键——更宽泛、更不精确
3. 分发双子星:Exclude / Extract
1
2
3
4
5
6
7
8
9
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 = T
4. 提取双子星:ReturnType / Parameters
1
2
3
4
5
6
7
8
9
10
11
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
// ⚠️ 重载函数的 ReturnType 只拿最后一个签名
5. NonNullable 与 never 陷阱
1
2
3
4
5
type MyNonNullable<T> = T extends null | undefined ? never : T;
type T1 = MyNonNullable<string | null>; // string ✅
type T2 = MyNonNullable<never>; // never ⚠️ ——面试高频!
// never 是空联合,分布式条件类型没有任何成员可供分发,短路返回 never
其实你每天都在用
- 表单数据抽字段:
Pick<User, 'name' | 'email'>只取表单需要的几个字段 - API 返回脱敏:
Omit<User, 'password' | 'salt'>去掉敏感字段再返回前端 - 组件 Props 透传:
Pick<ButtonHTMLAttributes, 'disabled' | 'type'>精准暴露原生属性 - Redux action 类型:
ReturnType<typeof createAction>自动推导 action 对象类型 - 路由参数封装:
Parameters<typeof router.push>拿到 push 参数包出去给业务用
常见误解(FAQ)
❌ 误区:「
Pick能按值类型筛选属性」 不能。Pick<T, K>只能按键名筛选。按值类型筛选需要用映射类型as子句:[K in keyof T as T[K] extends string ? K : never]。❌ 误区:「
Omit是 TS 原生关键字,有魔法优化」Omit不过是Pick<T, Exclude<keyof T, K>>的别名。面试手写 Omit 时注意——标准答案是用 Pick+Exclude 组合,不是直接写映射[P in Exclude<keyof T, K>](虽然 typescript 也接受后者)。❌ 误区:「
Exclude<非联合, U>的行为和联合一样」Exclude<string, string>结果是never。因为非联合不会分发,string extends string走 true 分支返回never。这和直觉可能不同。❌ 误区:「
ReturnType能提取任意类型」 泛型约束extends (...args: any) => any让非函数直接报错。此外,函数重载时只有最后一个重载签名参与条件类型推断——这是ReturnType的已知限制。
一句话总结
工具类型的源码 = 映射类型做 for-in + 条件类型做 if/else + infer 做模式匹配——三板斧倒背如流,面试手写题全是送分。