TypeScript 高级工具类型:从“类型世界”到“值世界”,一套组合拳搞定类型转换

TypeScript 高级工具类型:从"类型世界"到"值世界",一套组合拳搞定类型转换

摘要:TypeScript 的高级工具类型不是"语法糖",而是类型编程的核心武器。本文从"类型世界 vs 值世界"的根本认知出发,拆解 Pick、Omit、Partial、Record、ReturnType 等工具类型的原理与组合用法,帮你写出更健壮、更可维护的类型代码。

📑 目录

  • 两个世界:类型世界与值世界
  • typeof:连接两个世界的桥梁
  • 为什么需要工具类型?
  • 联合类型与 keyof:工具类型的基石
  • 对象类型工具:Pick、Omit、Partial
  • 联合类型工具:Exclude、Extract
  • 函数类型工具:ReturnType、Parameters
  • 映射类型工具:Record
  • 组合实战:从接口到 DTO 的完整类型设计
  • 互动讨论

两个世界:类型世界与值世界

TypeScript 中存在两个平行的世界:

世界 位置 操作对象 示例
类型世界 : 后面、<> 里面、type 声明、interface 类型 type T = string;
值世界 赋值语句、函数调用、console.log const x = 'hello';

这两个世界在编译时完全隔离。类型世界的内容在编译后会被擦除,不会出现在最终的 JavaScript 代码中;值世界的内容则会在运行时真实存在。

typescript

typescript 复制代码
// 类型世界
interface User {
  id: number;
  name: string;
}

// 值世界
const user: User = { id: 1, name: '大许' };

User 活在类型世界,user 活在值世界。它们共享同一个名字,但属于不同的维度。

typeof:连接两个世界的桥梁

typeof 在 JavaScript(运行时)和 TypeScript(编译时)里干的是完全不同的两件事:

typeof 出现的位置 作用 示例
值世界(JS 运行时) 返回变量的基本类型(字符串) typeof myFunc"function"
类型世界(TS 类型注解中) 转为类型定义 type T = typeof myFunc

核心结论: ReturnTypeParameters 等工具只吃"类型",不吃"值"。要处理具体函数时,必须先用 typeof 把它送进类型世界。

typescript

ini 复制代码
const foo = (a: string) => 123;

// ❌ 错误:foo 是值,不是类型
type Wrong = ReturnType<foo>;

// ✅ 正确:先用 typeof 把值转为类型
type Right = ReturnType<typeof foo>; // number

记忆口诀:在类型世界里,typeof 是"值→类型"的传送门。

为什么需要工具类型?

手写独立类型和用工具类型生成类型,最终结果在类型定义上是完全等价的 。但核心区别在于:你建立的是派生关系 还是独立副本

1. 维护成本:改一处,自动变

假设 User 接口在项目迭代中发生了变化:

typescript

typescript 复制代码
// 原来的定义
interface User {
  id: number;
  name: string;      // 后来改成了 fullName
  email: string;
}

// 版本升级后
interface User {
  id: number;
  fullName: string;  // 👈 字段名变了
  email: string;
}
  • 手写独立类型type UserBasic = { name: string; email: string; } 不会自动更新,必须手动全局搜索修改。
  • 使用工具类型type UserBasic = Pick<User, 'name' | 'email'>。TypeScript 会报错(name 不存在于 User 中),你只需把 'name' 改成 'fullName'一处修改,所有使用处自动同步

2. 组合能力:避免"类型爆炸"

假设你需要多种形态的 DTO:

typescript

ini 复制代码
type CreateUserDto = Pick<User, 'name' | 'email'>;
type UpdateUserDto = Partial<Pick<User, 'name' | 'email'>>;
type UserResponse = Omit<User, 'password'>;

User 加新字段时,只需确定"该不该出现在 DTO 里",修改对应的 PickOmit,派生关系让你永远不用去手动同步副本。

3. 语义表达:代码自文档化

当你看到 Pick<User, 'name' | 'email'> 时,一眼就知道这个类型是 User严格子集 。而手写类型无法从定义上看出和 User 的关联。

什么时候应该手写独立类型?

  • 该类型与现有类型没有关系(如全新的第三方 API 响应结构)
  • 该类型是聚合型,由多个无关类型拼凑而成
  • 希望该类型完全不依赖底层变化,即使源头变了也保持原样

联合类型与 keyof:工具类型的基石

联合类型(Union Type)

联合类型表示"值可以是这些类型中的任意一种",用管道符 | 表示。

typescript

ini 复制代码
let id: string | number;
id = "abc123"; // ✅
id = 456;      // ✅
id = true;     // ❌ boolean 不在联合类型中

类型收窄(Type Narrowing)

当变量是联合类型时,TypeScript 只允许调用所有类型共有的方法。要调用特定类型的方法,必须进行类型收窄:

typescript

typescript 复制代码
function printLength(value: string | number) {
  // ✅ 使用 typeof 收窄
  if (typeof value === "string") {
    console.log(value.length); // 这里 value 是 string
  } else {
    console.log(value.toFixed(2)); // 这里 value 是 number
  }
}

常用类型收窄方式:typeofininstanceofArray.isArrayswitch

keyof 操作符

keyof 是"类型世界"的操作符,用于把"对象类型"的所有键名提取为"联合类型"。

typescript

ini 复制代码
interface User {
  id: number;
  name: string;
  age: number;
}

type UserKeys = keyof User; // "id" | "name" | "age"

typeofkeyof 的分工

操作符 入口 出口 记忆锚点
typeof(类型世界) 一个值(变量/函数) 这个值的类型定义 "把值变成类型"
keyof 一个类型(对象类型) 属性名组成的联合类型 "提取对象的钥匙串"

对象类型工具:Pick、Omit、Partial

Pick<T, K> ------ 从 T 中挑选指定属性

typescript

ini 复制代码
interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

type UserPreview = Pick<User, "id" | "name">;
// 结果:{ id: number; name: string; }

const u1: UserPreview = {
  id: 1,
  name: "大许"
};

物理本质Pick 遍历 K 中的每个成员,从 T 中取出对应属性,组装成新类型。

参数

  • T:对象类型
  • Kkeyof T子集(联合类型)

Omit<T, K> ------ 从 T 中剔除指定属性

typescript

ini 复制代码
type UserSafe = Omit<User, "email">;
// 结果:{ id: number; name: string; age: number; }

const u2: UserSafe = {
  id: 2,
  name: "大刘",
  age: 18
};

实现原理Omit<T, K> = Pick<T, Exclude<keyof T, K>>

物理过程

  1. keyof T'id' | 'name' | 'age' | 'email'
  2. Exclude<联合类型, K>'id' | 'name' | 'age'
  3. Pick<T, 'id' | 'name' | 'age'>{ id: number; name: string; age: number; }

Partial<T> ------ 所有属性变为可选

typescript

css 复制代码
type PartialUser = Partial<User>;
// 结果:{ id?: number; name?: string; age?: number; email?: string; }

// 适合 PATCH 更新场景------客户端只传需要修改的字段
const patch: PartialUser = {
  name: "大锋",
  age: 18
};

参数T 是对象类型。

工具类型对比

工具 作用 参数 典型场景
Pick<T, K> 挑选指定属性 T 对象,K 联合类型 数据预览、列表展示
Omit<T, K> 剔除指定属性 T 对象,K 联合类型 隐藏敏感字段
Partial<T> 所有属性可选 T 对象 PATCH 更新、表单初始值

联合类型工具:Exclude、Extract

Exclude<T, U> ------ 从联合类型中排除成员

typescript

ini 复制代码
type All = "id" | "name" | "age" | "email";
type AfterExclude = Exclude<All, "email">;
// 结果:"id" | "name" | "age"

参数

  • T:联合类型
  • U:要排除的类型

Extract<T, U> ------ 从联合类型中提取成员

typescript

ini 复制代码
type All = "id" | "name" | "age" | "email";
type AfterExtract = Extract<All, "name" | "email">;
// 结果:"name" | "email"

Exclude vs Omit

| 对比 | Exclude<T, U> | Omit<T, K> |
|----------|------------------|--------------|--------|-------------------------------|
| 操作对象 | 联合类型(如 `'a' | 'b' | 'c'`) | 对象类型 (如 interface User) |
| 输出 | 联合类型 | 对象类型 |
| 典型场景 | 类型字符串过滤 | 对象属性剔除 |

函数类型工具:ReturnType、Parameters

ReturnType<T> ------ 提取函数的返回值类型

typescript

csharp 复制代码
function fn() {
  return { x: 1, y: 2 };
}

type FnReturn = ReturnType<typeof fn>;
// 结果:{ x: number; y: number; }

⚠️ 关键注意ReturnType 只接受类型 ,不接受 ,必须用 typeof

错误示例

typescript

rust 复制代码
// ❌ 报错:ReturnType 只接受函数类型
type Wrong = ReturnType<fn>;

// ✅ 正确:先用 typeof 把函数值转换为类型
type Right = ReturnType<typeof fn>;

Parameters<T> ------ 提取函数的参数类型(元组)

typescript

typescript 复制代码
function greet(name: string, age: number): void {}

type GreetParams = Parameters<typeof greet>;
// 结果:[string, number]

参数类型速查

工具 第一个参数 T 结果
ReturnType<T> 函数类型 函数返回值类型
Parameters<T> 函数类型 函数参数元组类型

映射类型工具:Record

Record<K, T> 用固定键和固定值类型构造一个对象类型。

typescript

typescript 复制代码
// 键为 string,值为 number
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };

// HTTP 状态码映射
type ErrorMsgMap = Record<number, string>;
const errorMessage: ErrorMsgMap = {
  400: "请求参数错误",
  401: "未登录,请重新登录",
  403: "权限不足,禁止访问",
  404: "找不到资源",
  500: "服务器内部错误",
};

function getErrMsg(code: number) {
  return errorMessage[code] ?? "未知错误";
}

参数

  • K:联合类型(作为键名)
  • T:类型(作为值类型)

组合实战:从接口到 DTO 的完整类型设计

将上述工具类型组合使用,可以构建一套完整的类型体系:

typescript

typescript 复制代码
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// 场景1:创建用户 DTO --- 只需要 name 和 email
type CreateUserDto = Pick<User, 'name' | 'email'>;

// 场景2:更新用户 DTO --- 所有字段都可选,且不能改 id 和 createdAt
type UpdateUserDto = Partial<Omit<User, 'id' | 'createdAt'>>;

// 场景3:从函数返回值中提取数据类型
function fetchUser(id: number): Promise<{ data: User; status: number }> {
  return Promise.resolve({
    data: { id: 1, name: '大许', email: 'xx@xx.com', password: '123', createdAt: new Date() },
    status: 200,
  });
}
type FetchUserReturn = ReturnType<typeof fetchUser>;

// 场景4:枚举值到描述的映射
type UserRole = 'admin' | 'user' | 'guest';
type RoleDescription = Record<UserRole, string>;
const roleDesc: RoleDescription = {
  admin: '管理员',
  user: '普通用户',
  guest: '访客',
};

互动讨论

💬 PickOmit 能接受 keyof T 之外的字符串吗?

不能。K 必须是 keyof T 的子集。如果传入 User 中不存在的键,TypeScript 会报错。

💬 Partial<T>{ [P in keyof T]?: T[P] } 有什么关系?

Partial<T> 内部就是用这个"映射类型"语法实现的。它是 TypeScript 内置的工具类型,本质上是一个预定义的泛型类型。

💬 为什么 ReturnType 不能直接传入函数,必须用 typeof

ReturnType 工作在类型世界 ,只接受类型 参数。函数名(如 fn)是一个 ,存在于值世界typeof fn 把值转换为类型,才能被 ReturnType 处理。

💬 ExcludeOmit 的核心区别是什么?

Exclude 处理联合类型 (如 'a' | 'b' | 'c'),Omit 处理对象类型 (如 interface User)。Omit 内部实际上用了 Exclude 来过滤键名。

💬 工具类型在实际项目中怎么用?

最常见的场景是DTO(数据传输对象) 设计。比如从 User 接口派生出 CreateUserDtoUpdateUserDtoUserResponse,避免重复定义,源头变化时自动同步。

相关推荐
JaydenAI5 小时前
[TypeScript学习笔记-05]作为一等成员的函数
typescript·function
常宇佳6 小时前
vue3 @代指src路径设置
前端·typescript·vue
常宇佳10 小时前
vue3 $refs使用方法
前端·vue.js·typescript
不好听6131 天前
TypeScript 面试必会:工具类型全家桶,从 keyof 到手写 Partial
面试·typescript
嘟嘟07171 天前
从入口到 CRUD:用 NestJS 的模块化与装饰器思想读懂一个后端应用
后端·typescript·nestjs
嘟嘟07171 天前
TypeScript 工具类型通关:Pick、Omit、Partial、Record 区别与 Omit 等价实现一次讲清
设计模式·面试·typescript
何时梦醒1 天前
TypeScript 工具类型一篇讲透:Pick、Omit、Partial、Exclude、Record、ReturnType、keyof
前端·面试·typescript
小林ixn1 天前
TS 工具类型实战:Pick/Omit/Partial/Record 一次讲透,别再 Omit 反了
typescript·代码规范