TypeScript 面试必会:工具类型全家桶,从 keyof 到手写 Partial

5 阅读7分钟

从一个真实的面试题开始

面试官甩过来一道题:

"Omit<T, k> 等价于 Pick<T, Exclude<keyof T, k>>,这句话你能解释一下吗?"

如果只是背结论,一追问就露馅。但如果你自己手写过一遍这个等价实现,就会知道它背后是三条环环相扣的链:keyof 取键 → Exclude 删键 → Pick 留键。这篇文章就用面试题的线索,把 TS 工具类型这一块串成一张网,面试问哪个都不慌。

先准备一张表,后面所有问题都在这张表上:

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

面试题一:Omit<T, k> 为什么等价于 Pick<T, Exclude<keyof T, k>>

把右边拆成三步,每一步都很直白:

keyof T                        Exclude<keyof T, "email">        Pick<T, 剩下的键>
   ↓                                ↓                                ↓
"id" | "name" | "age" | "email""id" | "name" | "age"   →  { id; name; age }
  1. keyof T 拿到所有键的联合类型:"id" | "name" | "age" | "email"
  2. Exclude 删掉要排除的键,剩下要保留的键:去掉 "email"
  3. Pick 把剩下的键从 T 里挑出来,形成新类型

这三步合起来,就是 Omit 的效果——去掉 email 字段。TS 内置的 Omit 实现就是这么写的:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

面试答案:Omit 是"删掉几个字段",而 Pick 是"保留几个字段",两者互为反面。Omit 的底层就是先 keyof 取全部键,再 Exclude 删掉不要的,最后 Pick 挑剩下的。删掉的反面就是保留剩下的。


面试题二:keyof 是工具类型吗?

不是。 这是一个常见的概念混淆。

  • keyof类型操作符(语法关键字,像 typeofininfer),它不"产生"一个工具,而是查询一个类型的所有键,返回联合类型。
  • 工具类型(Utility Types)是 TS 内置的一批"类型函数",比如 PickOmitPartial

所以你会发现一个规律:keyof 是一切对象型工具类型的地基PickOmitPartial 的手写实现,全部要从 keyof T 开始。


面试题三:工具类型有哪些?各自操作什么?

工具类型看起来多,但其实每一种都只操作某一类"对象"。按操作对象分类,一下就记住了:

操作"对象类型字段"的

工具类型作用示例
Pick<T, K>按键挑选若干字段Pick<User, "id" | "name">
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" | "b", "b">"a"
Extract<T, U>只保留两边都有的成员(交集)Extract<"a" | "b", "b" | "c">"b"
NonNullable<T>去掉 nullundefinedNonNullable<string | null>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

记忆框架:成对出现 + 按对象分类

对象类型字段:Partial ↔ Required、Readonly
对象类型键  :Pick ↔ Omit、Record(造)
联合类型    :Exclude ↔ Extract、NonNullable
函数类型    :ReturnType ↔ Parameters、InstanceType
异步        :Awaited

面试答案:TS 工具类型大多成对出现,记一个就记住它的反面。再按"操作对象"分类——对象类型、联合类型、函数类型、Promise——整套工具类型就织成网了,而不是一堆孤立的名字。


面试题四:手写一个 Partial(映射类型)

面试常让手写工具类型,本质只考一个语法:映射类型(Mapped Type)

type MyPartial<T> = { [K in keyof T]?: T[K] };

逐词拆解:

片段含义
T传入的对象类型,如 User
keyof T取所有键 → "id" | "name" | "age" | "email"
[K in keyof T]遍历:K 依次等于联合里的每个键
?:给每个属性加上可选修饰符
T[K]索引访问类型:取 K 对应的值类型

User 展开就是:

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"] 在类型层面读类型。

同样的骨架,换几个修饰符就是另一个工具类型:

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]: ... })管对象型工具类型,条件类型 + inferT extends U ? X : Y)管联合型和函数型。手写练习时,把这五个变体写一遍,映射类型就焊死在脑子里了。


面试题五:ReturnType<typeof fn> 里的 typeof 是什么?

这道题最容易卡住人,因为同一个 typeof 有两副面孔

场景写法结果
JS 运行时typeof fn字符串 "function"
TS 类型层面typeof fnfn 的类型 () => { x1: number; y: number }

ReturnType<typeof fn> 里,typeof 出现在类型位置,所以走的是类型查询——把一个fn)转成它的类型。为什么需要这一步?因为 ReturnType 只接收"类型"不接收"值",而 fn 是个值。

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

type MyReturnType<T extends (...args: any) => any>
  = T extends (...args: any) => infer R ? R : never;

面试答案:typeof 在类型位置是把值变成类型,ReturnType 是从函数类型里抠出返回值类型。两者合起来就是"让实现来推导返回类型",保持单一事实来源。


面试题六:??|| 有什么区别?

这题虽然不属于工具类型,但面试常挨着问。两者的默认值判断标准完全不同

  • ||假值(falsy):0""falseNaN 都会触发
  • ?? 只看 null / undefined
const n = 0;
const a = n ?? 10;   // 0   —— 0 不是 null/undefined,保留
const b = n || 10;   // 10  —— 0 是 falsy,被替换成默认值

实战场景(http 状态码映射):

const errorMessage: Record<number, string> = {
  400: "请求参数错误",
  401: "未登录,请先登录",
  404: "资源找不到",
};

function getErrMsg(code: number): string {
  return errorMessage[code] ?? "未知错误";  // 这里必须用 ??
}

如果某个 key 存在但值是 ""0 这类合法值,|| 会误判成"缺失"替换成默认值,?? 不会。

顺带把"带问号的运算符"全家桶补全:

写法名字作用
?.可选链链上安全访问,中途为空返回 undefined 不报错
??空值合并左边是 null/undefined 才取右边
??=空值合并赋值左边是空值才赋值(a ??= b
||= / &&=逻辑赋值左边是 falsy / truthy 才赋值
? :三元条件简写 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