文章

内置工具类型源码解读

内置工具类型源码解读

一句话概括

TypeScript 内置工具类型(PartialPickExcludeReturnType 等)面试手写率极高。它们的源码加起来不过几十行,核心是映射类型 + 分布式条件类型 + 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 做提取——这三板斧组合好了,没有拆不动的类型。

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