映射类型与内置工具类型

如果说泛型解决的是“一个类型模板如何适配不同输入”,那么映射类型解决的就是另一类高频问题:如何基于一个已有类型,系统性地批量改造出另一个类型。

这在真实项目里非常重要。因为几乎没有哪个业务模型只会以一种形态存在。一个 User 往往会派生出:

  • 新建表单输入类型
  • 更新接口类型
  • 列表项类型
  • 对外返回类型
  • 只读展示类型
  • 字段权限映射

如果你每次都手写一份新结构,不仅重复,而且极易失去同步。映射类型的价值,就是把这些“结构性变化”变成可复用的类型操作。

什么是映射类型

先看一个最基础的例子:

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

这段代码的意思是:

  • 遍历 T 的每一个键
  • K 依次拿到这些键
  • 对每个键对应的属性类型保持不变
  • 但把所有属性都改成可选

如果有:

interface User {
  id: number;
  name: string;
  active: boolean;
}

那么 Optional<User> 的结果就是:

{
  id?: number;
  name?: string;
  active?: boolean;
}

这就是映射类型的本质:按某种规则,批量遍历并改造一个类型的字段。

为什么映射类型这么适合真实业务

因为业务模型通常不是孤立存在的。比如一个用户模型:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: string;
}

它在不同场景里会有不同需求:

  • 创建用户时不该由前端传 id
  • 更新用户时很多字段可以局部提交
  • 返回前端时不该暴露 password
  • 展示详情时可能全字段只读

如果每种场景都重新写一个接口,代码会越来越重复。映射类型和工具类型,就是为这种“基于一个源模型派生多个视图”的需求而生的。

TypeScript 内置工具类型,本质上就是常用映射类型的封装

最常见的工具类型包括:

  • Partial<T>:把所有字段变成可选
  • Required<T>:把所有字段变成必填
  • Readonly<T>:把所有字段变成只读
  • Pick<T, K>:挑选部分字段
  • Omit<T, K>:排除部分字段
  • Record<K, V>:按键集合构造对象类型

先看两个非常实用的例子:

type CreateUserInput = Omit<User, "id" | "createdAt">;
type UpdateUserInput = Partial<Omit<User, "id" | "createdAt" | "password">>;

这两行已经足够覆盖很多业务接口设计。

PartialRequiredReadonly 的典型使用场景

Partial<T>

type UpdateUserInput = Partial<User>;

它适合局部更新、表单 patch 提交、配置覆盖等场景。含义非常直接:这个对象不一定一次性提供全部字段。

Required<T>

type CompleteUser = Required<UserDraft>;

它常用于“草稿阶段字段可缺失,正式提交时字段必须完整”的场景。

Readonly<T>

type UserView = Readonly<User>;

这在缓存快照、只读配置、不可变状态管理中很常见。它能帮助你表达“这个对象是给你看的,不是给你改的”。

PickOmit 是实际项目中极高频的组合工具

假设你有:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: string;
}

那么:

type UserPreview = Pick<User, "id" | "name" | "email">;
type PublicUser = Omit<User, "password">;

这类写法特别适合:

  • 列表页只展示部分字段
  • 接口响应排除敏感字段
  • 组件 props 只消费原模型的一部分

它们最大的价值不是省几行代码,而是明确告诉读者:这个类型是从哪个源模型裁剪出来的。

Record 是很多人低估的利器

type Permission = "read" | "write" | "delete";

const permissionLabel: Record<Permission, string> = {
  read: "读取",
  write: "写入",
  delete: "删除"
};

Record 的好处在于,它把“对象键必须完整覆盖某个联合类型”这件事变成了编译期约束。你只要漏写一个键,TypeScript 就会直接提醒你。

这在这些场景特别有用:

  • 角色到文案映射
  • 状态到组件映射
  • 权限到处理函数映射
  • 枚举值到显示标签映射

相比写一个普通对象,Record 更能表达“这里不是随便几个键,而是一个必须完整覆盖的映射关系”。

你也可以自己写工具类型

前面提到的 Optional<T> 就是最简单的自定义工具类型。再看两个常见写法:

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

这里的:

  • -readonly 表示去掉只读修饰符
  • -? 表示去掉可选修饰符

你不一定要一开始就背熟这些语法,但你要知道一件事:内置工具类型不是神秘黑盒,它们本质上就是少量映射能力的组合。

映射类型真正厉害的地方,是它在表达“派生关系”

这是非常重要的一层理解。很多人学工具类型时,只记成几个 API 名字,比如 PickOmitPartial。但你如果只记名字,很快就会忘。

更好的理解方式是:

  • Partial 表达“同一模型的可选版本”
  • Readonly 表达“同一模型的只读版本”
  • Pick 表达“从源模型中投影一部分字段”
  • Omit 表达“从源模型中排除某些字段”
  • Record 表达“从键集合到值类型的完整映射”

一旦你从“语法”切换到“派生关系”,这些工具会变得非常自然。

一个真实建模例子

interface Article {
  id: number;
  title: string;
  content: string;
  authorId: number;
  status: "draft" | "published";
  createdAt: string;
}

type CreateArticleInput = Omit<Article, "id" | "createdAt">;
type UpdateArticleInput = Partial<Omit<Article, "id" | "createdAt">>;
type ArticleCard = Pick<Article, "id" | "title" | "status">;
type ReadonlyArticle = Readonly<Article>;

从这个例子你可以看到,整个系统围绕一个核心模型派生出多个场景类型,而不是每个场景都独立抄一份结构。这样后续演化时,维护成本会低很多。

一个工程建议:先用内置工具,不要急着重复造轮子

很多初学者学会映射类型后,会开始写各种自定义工具。技术上当然可以,但工程上不一定划算。

更稳妥的顺序应该是:

  1. 先熟练掌握内置工具类型
  2. 只有当内置工具无法表达需求时,再自己写映射类型
  3. 自定义工具类型命名要清楚,别为了省几行代码引入更重的理解成本

因为大多数业务场景,其实已经被官方工具覆盖得差不多了。

初学者常见误区

误区一:派生类型全部手写

短期看快,长期看最容易失控。字段一变,五六处都得手动同步。

误区二:看见映射类型就觉得太高级,不敢用

其实你每天都可能在写它的结果,只是还没意识到这些“局部可选”“字段裁剪”本来就很适合自动化。

误区三:为了高级而写过度抽象的自定义工具

工具类型的目标是减少重复和提升清晰度,不是把简单需求改写成阅读门槛更高的类型体操。

本文小结

映射类型让你可以基于一个已有类型,按规则批量生成另一个类型;内置工具类型则是这些常见规则的现成封装。掌握它们之后,你写类型的方式会从“复制粘贴多个相似接口”,升级为“围绕核心模型系统派生多个场景类型”。

这不仅更省代码,更重要的是,它让类型之间的关系更清楚、更稳定、更适合长期维护。

练习

  1. PickUser 中挑出 idname,再解释为什么这比手写一个新接口更好维护。
  2. Omit 构造一个不包含 password 的返回类型,并为用户列表页再派生一个精简展示类型。
  3. Record 定义一份角色到中文名称的映射,再试着漏掉一个键,观察 TypeScript 的提示。

后记

2026年5月21日于上海。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐