文章

内置工具类型源码解读深度解析

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 做模式匹配——三板斧倒背如流,面试手写题全是送分。

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