【TypeScript】让类型体操更简单:TypeScript 实用工具类型精讲

所属专栏: 《前端小技巧集合:让你的代码更优雅高效》
上一篇: 【Git】Git 骚操作:rebase, cherry-pickreflog 让你成为团队高手
作者: 码力无边


引言:当你的类型定义,也需要“七十二变”

嘿,各位在类型世界里追求严谨与优雅的道友们,我是码力无边

在我们的前端修行之路上,TypeScript 就像是我们获得的一本“天书”,它为我们原本自由不羁的 JavaScript 带来了“秩序”与“法则”。通过类型定义,我们能在编码阶段就消灭掉大量的潜在 bug,让大型应用的维护变得前所未有的轻松。

我们勤勤恳恳地为数据定义 interfacetype,就像是在为我们的代码世界构建精准的“模型”。

interface User {
  id: number;
  name: string;
  email: string;
  avatarUrl?: string; // 可选属性
  createdAt: Date;
}

这个 User 模型很完美。但很快,新的“妖魔”(需求)就出现了:

  • 场景一:我们需要创建一个“更新用户”的函数 updateUser。这个函数接收一个 user 对象,但这个对象里的属性都是可选的,因为用户可能只更新 name,也可能只更新 avatarUrl。难道我们要重新定义一个所有属性都带 ?UpdateUserPayload 类型吗?
  • 场景二:我们需要渲染一个“用户头像列表”,这个组件只需要用户的 idavatarUrl。难道我们要为此再定义一个只包含这两个属性的 UserAvatar 类型吗?
  • 场景三:我们需要一个“用户注册”的表单数据类型,它应该包含 User 的所有属性,除了 idcreatedAt,因为这两个字段是后端生成的。难道我们又要手动复制粘贴,然后删掉两个属性来创建一个 UserRegistration 类型吗?

如果每遇到一个新场景,我们都要通过“复制粘贴大法”来创造一个新的、略有差异的类型,那么我们的类型定义很快就会变得冗余、难以维护、且彼此之间失去了关联。一旦源头 User 接口发生变化(比如增加一个 age 属性),我们就必须手动去同步所有由它“衍生”出来的类型,这简直是一场灾难。

为了避免这种“类型爆炸”,TypeScript 的创造者们早已为我们准备好了一套“七十二变”的仙法——实用工具类型 (Utility Types)

这些内置的工具类型,就像是类型世界里的“高阶函数”。它们接收一个已有的类型作为“输入”,然后经过一番“点化”,生成一个符合我们需求的“新类型”。

今天,码力无边就将带你深入学习几个最常用、最强大的实用工具类型,包括 Partial, Required, Readonly, Pick, Omit, Record 等。掌握它们,你将能够像做“类型体操”一样,灵活地组合、变换、裁剪你的类型,写出更简洁、更智能、更具可维护性的 TypeScript 代码。

一、让属性“可选”或“必选”:Partial<T>Required<T>

1. Partial<T>:全体起立,变成可选!

Partial<T> (Partial: 部分的) 能将一个类型 T 的所有属性都变成可选的(在属性名后加上 ?)。

这完美地解决了我们引言中的场景一

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

// 使用 Partial<User> 生成一个新类型
type UserUpdatePayload = Partial<User>;
// 相当于:
// type UserUpdatePayload = {
//   id?: number;
//   name?: string;
//   email?: string;
// }

function updateUser(id: number, payload: UserUpdatePayload) {
  // ... a.pi.updateUser(id, payload)
}

// 现在,你可以传入只包含部分属性的对象,类型检查会通过!
updateUser(1, { name: '码力加强版' });
updateUser(2, { email: 'new@example.com' });
2. Required<T>:一个都不能少!

Required<T>Partial<T> 相反,它能将一个类型 T 的所有属性(包括原本可选的)都变成必选的

interface UserProfile {
  name: string;
  avatarUrl?: string; // 原本是可选的
  bio?: string;
}

// 经过 Required 处理后
type CompleteUserProfile = Required<UserProfile>;
// 相当于:
// type CompleteUserProfile = {
//   name: string;
//   avatarUrl: string; // 变成必选了!
//   bio: string;
// }

function processProfile(profile: CompleteUserProfile) {
  // 函数内部可以放心地访问 profile.avatarUrl,无需做非空判断
  console.log(profile.avatarUrl.toUpperCase()); 
}

Required<T> 在你需要确保一个对象的所有字段都已被填充的场景下非常有用。

二、属性的“生杀大权”:Pick<T, K>Omit<T, K>

这两个工具类型,给了我们从一个已有类型中“挑拣”或“剔除”属性的能力。

1. Pick<T, K>:精准“挑拣”我想要的

Pick<T, K> (Pick: 挑选) 能从类型 T 中,挑选出 K 中指定的键 (Keys),来构造一个新的类型。K 必须是 T 中存在的键,通常是一个联合类型字符串。

这完美地解决了我们的场景二

interface User {
  id: number;
  name: string;
  email: string;
  avatarUrl?: string;
}

// 只挑选 'id' 和 'avatarUrl' 两个属性
type UserAvatarData = Pick<User, 'id' | 'avatarUrl'>;
// 相当于:
// type UserAvatarData = {
//   id: number;
//   avatarUrl?: string;
// }

const avatars: UserAvatarData[] = [
  { id: 1, avatarUrl: '...' },
  { id: 2, avatarUrl: '...' }
];
2. Omit<T, K>:果断“剔除”我不想要的

Omit<T, K> (Omit: 忽略,删除) 与 Pick 相反,它会构造一个新类型,这个新类型拥有 T 的所有属性,除了 K 中指定的那些键。

这完美地解决了我们的场景三

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

// 剔除 'id', 'passwordHash', 'createdAt' 这三个后端管理的字段
type UserRegistrationForm = Omit<User, 'id' | 'passwordHash' | 'createdAt'>;
// 相当于:
// type UserRegistrationForm = {
//   name: string;
//   email: string;
// }

const form: UserRegistrationForm = {
  name: '新用户',
  email: 'user@example.com'
};

PickOmit 是处理接口数据 DTO (Data Transfer Object) 转换时最得力的助手。它们让你能够基于一个“全量”的基础模型,轻松地派生出用于不同业务场景的“精简”模型。

三、其他常用工具类型速览

1. Readonly<T>:筑起“不可变”的长城

Readonly<T> 能将一个类型 T 的所有属性都设为只读。任何尝试修改这些属性的行为,都会在编译时被 TypeScript 拦下。

interface Config {
  apiKey: string;
  apiUrl: string;
}

const config: Readonly<Config> = {
  apiKey: 'abc-123',
  apiUrl: 'https://api.example.com'
};

// 下面这行代码会直接报错!
// Cannot assign to 'apiKey' because it is a read-only property.
// config.apiKey = 'new-key'; 

这在处理应用配置、或者在函数式编程中强调数据不可变性时非常有用。

2. Record<K, T>:创建“字典”类型的捷径

Record<K, T> (Record: 记录) 是一个非常强大的工具,用于快速创建一个对象类型,它的键 (Keys) 是类型 K值 (Values) 是类型 T

场景: 我们需要一个对象,它的键是字符串(比如页面路由),值是对应的页面配置对象。

interface PageInfo {
  title: string;
  requiresAuth: boolean;
}

type Pages = 'home' | 'about' | 'contact';

// 使用 Record 创建一个类型,键必须是 'home', 'about', 'contact' 之一
// 值必须符合 PageInfo 接口
const pagesConfig: Record<Pages, PageInfo> = {
  home: { title: '首页', requiresAuth: false },
  about: { title: '关于我们', requiresAuth: false },
  contact: { title: '联系我们', requiresAuth: true },
  // 如果你写了不存在的键,比如 `dashboard: { ... }`,会报错
  // 如果值的类型不对,比如 `home: { title: 123 }`,也会报错
};

Record 为创建结构统一的“字典”或“哈希表”类型提供了极大的便利和类型安全。

3. Exclude<T, U>Extract<T, U>:联合类型的“交集”与“差集”

这两个工具专门用于处理联合类型 (Union Types)

  • Exclude<T, U> (Exclude: 排除): 从 T 中排除所有可以赋值给 U 的类型。相当于计算 TU差集
  • Extract<T, U> (Extract: 提取): 从 T 中提取所有可以赋值给 U 的类型。相当于计算 TU交集
type AllStatus = 'pending' | 'processing' | 'success' | 'failed';
type FinalStatus = 'success' | 'failed';

// 从所有状态中,排除掉最终状态,得到正在进行的状态
type OngoingStatus = Exclude<AllStatus, FinalStatus>; // 'pending' | 'processing'

// 从所有状态中,提取出数字类型的状态(假设有)
type NumberStatus = Extract<All-Status | 1 | 2, number>; // 1 | 2

写在最后:从“类型定义者”到“类型 arquitetos”

TypeScript 的实用工具类型,是引领我们从一个单纯的“类型定义者”,走向一个更高级的“类型 arquitetos”的桥梁。

我们不再需要为每一个细微的差异而去重复地创造类型。相反,我们学会了基于已有的基础类型,通过组合、变换、裁剪等“体操动作”,以一种声明式的方式,派生出我们所需要的新类型。

这种编程方式,我们称之为类型编程 (Type Programming)。它让我们的类型定义变得:

  • DRY (Don’t Repeat Yourself):避免了冗余。
  • 可维护:修改源头类型,所有衍生类型自动更新。
  • 表达力强:类型本身就在讲述它们之间的关系和派生逻辑。

所以,道友们,请将这些强大的“仙法”融入你的日常修行。当你下一次需要创建一个新类型时,请先扪心自问:“我能否通过这些实用工具,从一个已有的类型‘变’出来?”

当你开始这样思考时,你就真正地掌握了 TypeScript 的精髓,你的代码也将达到一个新的、严谨与优雅并存的境界。


专栏预告与互动:

我们已经掌握了 TypeScript 的一些实用技巧。但要真正发挥 TS 的威力,我们需要理解它最核心、最强大的特性之一——泛型 (Generics)。泛型是如何让我们创建出可复用的、类型安全的组件和函数的?

  • 下一篇,我们将深入探索 TypeScript 的灵魂——泛型编程。你将学会如何定义泛型函数、泛型接口和泛型类,让你的代码在保持类型安全的同时,具备极致的灵活性!

感觉码力无边的“类型体操”让你对 TypeScript 的理解焕然一新?别忘了点赞、收藏、关注,你的每一次支持,都是我揭开 TypeScript 更深层奥秘的强大动力!

今日论道: Omit<T, K> 看起来非常有用,但它并不是 TypeScript 最早内置的工具类型之一。在没有 Omit 的年代,你能否只用 PickExclude 来组合实现一个功能完全一样的 MyOmit<T, K> 类型呢?这是一个经典的类型体操挑战,把你的解法或思路分享在评论区,我们一起探讨!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐