从一个真实的面试题开始
面试官甩过来一道题:
"
Omit<T, k>等价于Pick<T, Exclude<keyof T, k>>,这句话你能解释一下吗?"
如果只是背结论,一追问就露馅。但如果你自己手写过一遍 这个等价实现,就会知道它背后是三条环环相扣的链:keyof 取键 → Exclude 删键 → Pick 留键。这篇文章就用面试题的线索,把 TS 工具类型这一块串成一张网,面试问哪个都不慌。
先准备一张表,后面所有问题都在这张表上:
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
面试题一:Omit<T, k> 为什么等价于 Pick<T, Exclude<keyof T, k>>?
把右边拆成三步,每一步都很直白:
bash
keyof T Exclude<keyof T, "email"> Pick<T, 剩下的键>
↓ ↓ ↓
"id" | "name" | "age" | "email" → "id" | "name" | "age" → { id; name; age }
keyof T拿到所有键 的联合类型:"id" | "name" | "age" | "email"Exclude删掉要排除的键 ,剩下要保留的键:去掉"email"Pick把剩下的键从 T 里挑出来,形成新类型
这三步合起来,就是 Omit 的效果------去掉 email 字段。TS 内置的 Omit 实现就是这么写的:
ts
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
面试答案:Omit 是"删掉几个字段",而 Pick 是"保留几个字段",两者互为反面。Omit 的底层就是先
keyof取全部键,再Exclude删掉不要的,最后Pick挑剩下的。删掉的反面就是保留剩下的。
面试题二:keyof 是工具类型吗?
不是。 这是一个常见的概念混淆。
- keyof 是类型操作符 (语法关键字,像
typeof、in、infer),它不"产生"一个工具,而是查询一个类型的所有键,返回联合类型。 - 工具类型 (Utility Types)是 TS 内置的一批"类型函数",比如
Pick、Omit、Partial。
所以你会发现一个规律:keyof 是一切对象型工具类型的地基 。Pick、Omit、Partial 的手写实现,全部要从 keyof T 开始。
面试题三:工具类型有哪些?各自操作什么?
工具类型看起来多,但其实每一种都只操作某一类"对象"。按操作对象分类,一下就记住了:
操作"对象类型字段"的
| 工具类型 | 作用 | 示例 |
|---|---|---|
Pick<T, K> |
按键挑选若干字段 | `Pick<User, "id" |
Omit<T, K> |
按键去掉若干字段 | Omit<User, "email"> |
Partial<T> |
所有字段变可选 | Partial<User> |
Required<T> |
所有字段变必选(Partial 反面) | Required<Partial<User>> |
Readonly<T> |
所有字段变只读 | Readonly<Config> |
Record<K, V> |
用键类型 + 值类型现造一个对象类型 | Record<number, string> |
操作"联合类型"的
| 工具类型 | 作用 | 示例 |
|---|---|---|
Exclude<T, U> |
删掉 U 中出现的成员(差集) | `Exclude<"a" |
Extract<T, U> |
只保留两边都有的成员(交集) | `Extract<"a" |
NonNullable<T> |
去掉 null 和 undefined |
`NonNullable<string |
操作"函数类型"的
| 工具类型 | 作用 | 示例 |
|---|---|---|
ReturnType<T> |
提取函数返回值类型 | ReturnType<typeof fn> |
Parameters<T> |
提取函数参数类型(元组) | Parameters<(a: string) => void> → [string] |
InstanceType<T> |
提取构造函数的实例类型 | InstanceType<typeof Date> |
解 Promise 的
| 工具类型 | 作用 | 示例 |
|---|---|---|
Awaited<T> |
把 Promise<T> 拆成 T(可多层) |
Awaited<Promise<Promise<string>>> → string |
记忆框架:成对出现 + 按对象分类
vbnet
对象类型字段:Partial ↔ Required、Readonly
对象类型键 :Pick ↔ Omit、Record(造)
联合类型 :Exclude ↔ Extract、NonNullable
函数类型 :ReturnType ↔ Parameters、InstanceType
异步 :Awaited
面试答案:TS 工具类型大多成对出现,记一个就记住它的反面。再按"操作对象"分类------对象类型、联合类型、函数类型、Promise------整套工具类型就织成网了,而不是一堆孤立的名字。
面试题四:手写一个 Partial(映射类型)
面试常让手写工具类型,本质只考一个语法:映射类型(Mapped Type)。
ts
type MyPartial<T> = { [K in keyof T]?: T[K] };
逐词拆解:
| 片段 | 含义 |
|---|---|
T |
传入的对象类型,如 User |
keyof T |
取所有键 → `"id" |
[K in keyof T] |
遍历:K 依次等于联合里的每个键 |
?: |
给每个属性加上可选修饰符 |
T[K] |
索引访问类型:取 K 对应的值类型 |
对 User 展开就是:
ts
type MyPartial<User> = {
id?: number; // K = "id", T["id"] = number
name?: string; // K = "name", T["name"] = string
age?: number; // K = "age", T["age"] = number
email?: string; // K = "email", T["email"] = string
};
[K in keyof T] 是"循环",T[K] 在循环里逐个取值类型,?: 统一加可选。T[K] 索引访问类型 是一个单独的重要概念:像 user["name"] 读值一样,User["name"] 在类型层面读类型。
同样的骨架,换几个修饰符就是另一个工具类型:
ts
type MyPartial<T> = { [K in keyof T]?: T[K] }; // 全部可选
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }; // 全部只读
type MyRequired<T> = { [K in keyof T]-?: T[K] }; // 全部必选(-? 去掉可选)
type MyPick<T, K extends keyof T> = { [P in K]: T[P] }; // 只挑 K 这些键
type MyRecord<K extends keyof any, V> = { [P in K]: V }; // 现造一个对象
面试答案:工具类型的底层语法就两样------映射类型 (
{ [K in keyof T]: ... })管对象型工具类型,条件类型 + infer (T extends U ? X : Y)管联合型和函数型。手写练习时,把这五个变体写一遍,映射类型就焊死在脑子里了。
面试题五:ReturnType<typeof fn> 里的 typeof 是什么?
这道题最容易卡住人,因为同一个 typeof 有两副面孔:
| 场景 | 写法 | 结果 |
|---|---|---|
| JS 运行时 | typeof fn |
字符串 "function" |
| TS 类型层面 | typeof fn |
fn 的类型 () => { x1: number; y: number } |
在 ReturnType<typeof fn> 里,typeof 出现在类型位置,所以走的是类型查询 ------把一个值 (fn)转成它的类型 。为什么需要这一步?因为 ReturnType 只接收"类型"不接收"值",而 fn 是个值。
ts
function fn() {
return { x1: 1, y: 2 };
}
// typeof fn = () => { x1: number; y: number }
type fnReturn = ReturnType<typeof fn>;
// fnReturn = { x1: number; y: number }
ReturnType 做的事就是"把这个函数类型里返回值的类型抠出来"。为什么不直接手写 { x1: number; y: number }?因为函数返回类型一变,手写的地方容易漏改。用 ReturnType 让实现成为唯一事实来源 ,类型跟着走。ReturnType 的底层实现靠 infer:
ts
type MyReturnType<T extends (...args: any) => any>
= T extends (...args: any) => infer R ? R : never;
面试答案:
typeof在类型位置是把值变成类型,ReturnType是从函数类型里抠出返回值类型。两者合起来就是"让实现来推导返回类型",保持单一事实来源。
面试题六:?? 和 || 有什么区别?
这题虽然不属于工具类型,但面试常挨着问。两者的默认值判断标准完全不同:
||看假值 (falsy):0、""、false、NaN都会触发??只看null/undefined
ts
const n = 0;
const a = n ?? 10; // 0 ------ 0 不是 null/undefined,保留
const b = n || 10; // 10 ------ 0 是 falsy,被替换成默认值
实战场景(http 状态码映射):
ts
const errorMessage: Record<number, string> = {
400: "请求参数错误",
401: "未登录,请先登录",
404: "资源找不到",
};
function getErrMsg(code: number): string {
return errorMessage[code] ?? "未知错误"; // 这里必须用 ??
}
如果某个 key 存在但值是 "" 或 0 这类合法值,|| 会误判成"缺失"替换成默认值,?? 不会。
顺带把"带问号的运算符"全家桶补全:
| 写法 | 名字 | 作用 |
|---|---|---|
?. |
可选链 | 链上安全访问,中途为空返回 undefined 不报错 |
?? |
空值合并 | 左边是 null/undefined 才取右边 |
??= |
空值合并赋值 | 左边是空值才赋值(a ??= b) |
| ` | =/&&=` |
|
? : |
三元条件 | 简写 if/else 表达式 |
(注意:?= 不是合法的 JS/TS 运算符,别被面试题绕进去。)
面试答案:
??只在null/undefined时取默认值,||在所有 falsy 值都取------处理0、""、false这类合法值时用??更安全。
本篇总结
| 面试题 | 一句话答案 |
|---|---|
| Omit 的等价实现 | Omit = Pick 挑剩下的 = 先 keyof 取键,再 Exclude 删键,最后 Pick 留键 |
| keyof 是工具类型吗 | 不是,是类型操作符,是对象型工具类型的地基 |
| 工具类型有哪些 | 按对象分类:对象字段 / 联合类型 / 函数类型 / Promise,且大多成对 |
| 手写 Partial | 映射类型 { [K in keyof T]?: T[K] },换修饰符就是 Required/Readonly/Pick |
| ReturnType 里的 typeof | 类型层面的 typeof 把"值"变成"类型",ReturnType 抠出返回值 |
| ?? 和 || | ?? 只看 null/undefined,|| 看所有 falsy |