Appearance
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 的核心工具:基础类型、函数、interface
、type
以及强大的泛型。我们已经能够熟练地定义类型。
从今天开始,我们将迈入一个新境界:操作和转换类型。如果说之前我们是类型的"使用者",那么现在我们将成为类型的"创造者"和"魔术师"。这在社区中常被戏称为"类型体操",而我们将要学习的,就是这套体操中最实用、最核心的几个动作。
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>
- 挑出你想要的属性
场景:你只想展示用户的部分信息,比如一个只包含 id
和 name
的用户列表。
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
(即 T
是 U
的子集),那么最终的类型就是 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 详解、声明文件与框架集成》 中,我们将把所有知识融会贯通,解决在实际项目中一定会遇到的问题。