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

从一个真实的面试题开始

面试官甩过来一道题:

"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 }
  1. keyof T 拿到所有键 的联合类型:"id" | "name" | "age" | "email"
  2. Exclude 删掉要排除的键 ,剩下要保留的键:去掉 "email"
  3. 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类型操作符 (语法关键字,像 typeofininfer),它不"产生"一个工具,而是查询一个类型的所有键,返回联合类型。
  • 工具类型 (Utility Types)是 TS 内置的一批"类型函数",比如 PickOmitPartial

所以你会发现一个规律:keyof 是一切对象型工具类型的地基PickOmitPartial 的手写实现,全部要从 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> 去掉 nullundefined `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]: ... })管对象型工具类型,条件类型 + inferT 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""falseNaN 都会触发
  • ?? 只看 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
相关推荐
禁止摆烂_才浅1 小时前
前端 AI 面试题
前端·面试·ai编程
王中阳Go1 小时前
做了 3 年 Spring Boot,老板让我一周搭个 AI Agent,上线当天被安全叫停:Java 团队落地 AI 的三道坎,我替你踩完了
后端·面试
禁止摆烂_才浅1 小时前
Vite 面试题
前端·面试·vite
禁止摆烂_才浅1 小时前
微信小程序高频面试题
前端·面试·微信小程序
禁止摆烂_才浅1 小时前
Vue2 高频面试题
前端·vue.js·面试
禁止摆烂_才浅1 小时前
前端性能优化面试题
前端·面试·性能优化
嘟嘟07171 小时前
从入口到 CRUD:用 NestJS 的模块化与装饰器思想读懂一个后端应用
后端·typescript·nestjs
禁止摆烂_才浅1 小时前
Vue3 高频面试题
前端·vue.js·面试
何时梦醒1 小时前
CSS 三栏布局与 BFC 深度解析:Float、Flex、Grid 三种方案一网打尽
前端·css·面试