Skip to content

TypeScript 入门到实战(五):高级类型进阶 - 掌握 Utility Types、条件类型与 keyof

摘要: 本文带领读者从基础迈向高级,探索 TypeScript 强大的类型编程能力。文章将重点介绍官方内置的工具类型(Utility Types,如 Partial, Pick, Omit),它们能极大地简化日常的类型转换。同时,我们还将深入学习 keyof、条件类型(T extends U ? X : Y)和 infer 等高级工具,解锁"类型体操"的奥秘,让类型真正为你所用。

关键词: TypeScript 高级类型, Utility Types, 条件类型, keyof, infer, Partial, Pick, Omit, 映射类型, 类型体操


在前四篇文章中,我们已经掌握了 TypeScript 的核心工具:基础类型、函数、interfacetype 以及强大的泛型。我们已经能够熟练地定义类型。

从今天开始,我们将迈入一个新境界:操作和转换类型。如果说之前我们是类型的"使用者",那么现在我们将成为类型的"创造者"和"魔术师"。这在社区中常被戏称为"类型体操",而我们将要学习的,就是这套体操中最实用、最核心的几个动作。

1. keyof - 获取类型的"钥匙"

在深入了解工具类型之前,我们必须先掌握一个关键的操作符:keyof。它的作用是获取一个类型所有公有属性名联合类型

听起来有点绕?看个例子就明白了:

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

// UserKeys 的类型是 "id" | "name" | "email"
type UserKeys = keyof User; 

const key1: UserKeys = "id";    // OK
const key2: UserKeys = "name";  // OK
// const key3: UserKeys = "age"; // 错误: 不能将类型""age""分配给类型"UserKeys"

keyof 就像一把能打开 User 类型并取出所有"钥匙"(属性名)的工具。它确保了我们只能使用对象上真实存在的属性名,这在后续的类型操作中至关重要。

2. 内置工具类型 - 你的"瑞士军刀"

TypeScript 官方提供了一系列内置的工具类型(Utility Types),它们就像一个类型的"标准库",可以极大简化我们对类型的操作。让我们来看看最常用的几个。

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

场景:更新用户信息时,我们可能只想传入需要修改的字段,而不是整个 User 对象。

typescript
function updateUser(user: User, fieldsToUpdate: Partial<User>): User {
  return { ...user, ...fieldsToUpdate };
}

const currentUser: User = { id: 1, name: "Alice", email: "alice@example.com" };

const updatedUser = updateUser(currentUser, { name: "Alice Smith" });
// fieldsToUpdate 的类型是 { id?: number; name?: string; email?: string; }
// 只传入 name 是完全合法的

Readonly<T> - 将所有属性变为只读

场景:当你希望创建一个对象的"快照",并确保它在任何情况下都不被修改时。

typescript
const readonlyUser: Readonly<User> = {
  id: 2,
  name: "Bob",
  email: "bob@example.com",
};

// readonlyUser.name = "Robert"; // 错误: 无法为"name"赋值,因为它是只读属性。

Pick<T, K> - 挑出你想要的属性

场景:你只想展示用户的部分信息,比如一个只包含 idname 的用户列表。

typescript
// 从 User 类型中,挑选出 "id" 和 "name" 这两个键
type UserSummary = Pick<User, "id" | "name">;

const summary: UserSummary = {
  id: 3,
  name: "Charlie",
  // email: "charlie@example.com" // 错误:类型"{ id: number; name: string; email: string; }"的参数不能赋给类型"UserSummary"
};

注意,Pick 的第二个泛型参数必须是第一个泛型参数的 keyof 结果的子集。

Omit<T, K> - 排除不想要的属性

场景:与 Pick 相反,当你需要一个对象的绝大部分属性,只想排除个别敏感或不需要的字段时。

typescript
// 从 User 类型中,排除 "email" 这个键
type UserForPublicDisplay = Omit<User, "email">;

const publicUser: UserForPublicDisplay = {
  id: 4,
  name: "David",
  // email: "..." // 不能包含 email 属性
};

Record<K, T> - 创建字典类型

场景:当你需要创建一个对象,它的键是某种特定类型,值是另一种特定类型时。非常适合用来创建字典或映射表。

typescript
// 键是 string 类型 (用户名),值是 User 类型
type UserRegistry = Record<string, User>;

const registry: UserRegistry = {
  "alice": { id: 1, name: "Alice", email: "..." },
  "bob": { id: 2, name: "Bob", email: "..." },
  // "charlie": 123 // 错误:值必须是 User 类型
};

3. 条件类型 - 类型的"if-else"

条件类型让 TypeScript 的类型系统拥有了逻辑判断的能力。它的语法和 JavaScript 的三元运算符非常相似:

typescript
T extends U ? X : Y

意思是:如果类型 T 可以赋值给类型 U(即 TU 的子集),那么最终的类型就是 X,否则就是 Y

看个简单的例子:

typescript
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // -> true
type B = IsString<123>;     // -> false

一个更实用的例子是"解包"数组类型:

typescript
type Flatten<T> = T extends any[] ? T[number] : T;
// T[number] 可以获取数组元素的联合类型

type StrArray = string[];
type Num = number;

type FlattenedStr = Flatten<StrArray>; // -> string
type FlattenedNum = Flatten<Num>;     // -> number

4. infer - 在类型中"推断"

infer 是条件类型中的点睛之笔,它允许我们在类型判断的过程中"推断"并捕获某个部分的类型,并将其存储在一个新的类型变量中。

我们用 infer 来重写上面的 Flatten 类型,这是一种更通用的写法:

typescript
type FlattenWithInfer<T> = T extends (infer Item)[] ? Item : T;

infer Item 的意思是:"如果 T 是一个数组,请不要只告诉我它是数组,而是把它内部元素的类型推断出来,并命名为 Item,然后我就可以在 true 的分支里使用 Item 这个类型了。"

infer 最经典的用途是获取函数的返回值类型:

typescript
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
// 如果 T 是一个函数,就推断它的返回值类型 R,然后返回 R

function sayHello() { return "hello"; }
type ReturnOfSayHello = GetReturnType<typeof sayHello>; // -> string

type NumReturn = GetReturnType<() => number>; // -> number
type StrReturn = GetReturnType<(x: string) => string>; // -> string

ReturnType<T> 其实就是 TypeScript 内置的一个基于此原理的工具类型。

总结与展望

恭喜你,你已经完成了本次"类型体操"训练!我们学习了如何使用 keyof 获取类型的键,掌握了一系列实用的内置工具类型(Partial, Pick 等),并初步领略了条件类型和 infer 带来的动态类型编程能力。

这些高级工具能让你在面对复杂多变的业务需求时,写出更灵活、更精准、更易于维护的类型定义。

至此,我们已经将 TypeScript 语言本身的十八般武艺学了个遍。理论知识已经储备充足,是时候把它应用到真实世界了!

在系列的最后一篇文章 《TypeScript 入门到实战(六):真实项目配置 - tsconfig 详解、声明文件与框架集成》 中,我们将把所有知识融会贯通,解决在实际项目中一定会遇到的问题。