TypeScript 高级类型:条件类型与映射类型
TypeScript 的类型系统功能强大,除了基础的类型注解外,还提供了条件类型和映射类型这两个「高级武器」。掌握它们后,你可以编写出更灵活、更精确的类型定义,让代码在编译阶段就捕获更多潜在错误。
一、条件类型:类型界的「三元运算符」
1.1 为什么需要条件类型
传统的类型定义是静态的、固定不变的。比如你定义 type ID = string | number,这个类型永远是字符串或数字的并集。但实际开发中,你经常需要根据某个类型来推断另一个类型——这正是条件类型的用武之地。
条件类型的核心思想与 JavaScript 的三元运算符完全一致:条件 ? 真时的类型 : 假时的类型。只不过它工作在类型层面。
1.2 基本语法
// 基础语法结构
type 条件类型名<T> = T extends 目标类型 ? 结果类型A : 结果类型B;
extends 关键字在这里表达的是「是否可赋值给」的意思。TypeScript 会判断 T 是否可以赋值给目标类型,从而选择返回结果类型 A 或 B。
// 一个简单示例
type IsString<T> = T extends string ? true : false;
// 测试
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<"hello">; // true(字符串字面量也符合条件)
1.3 分布式条件类型
当条件类型的泛型参数是联合类型时,TypeScript 会自动「拆开」联合类型,分别判断每个成员,最终结果也是各个判断结果的联合。这种特性称为分布式条件类型。
// 分布式条件类型示例
type ToArray<T> = T extends any ? T[] : never;
// 联合类型会被拆解处理
type Result = ToArray<string | number>;
// 等价于: (string[]) | (number[])
注意这里使用了 extends any,这是一个常见的技巧,用来触发分布式行为。实际开发中,如果你想让自己的条件类型支持联合类型参数的分布式处理,这个模式非常实用。
1.4 使用 infer 进行类型推断
infer 是条件类型中最强大的关键字,它允许你从类型中「提取」某部分信息,类似于正则表达式的捕获组。
// 从函数类型中提取返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// 测试
type Func = () => number;
type Result = ReturnType<Func>; // number
// 从数组类型中提取元素类型
type ElementType<T> = T extends (infer E)[] ? E : never;
type StringArray = string[];
type ArrayResult = ElementType<StringArray>; // string
// 提取函数第一个参数的类型
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
type FuncWithTwoArgs = (a: string, b: number) => void;
type FirstArgResult = FirstArg<FuncWithTwoArgs>; // string
通过 infer,你可以在类型层面「解构」复杂类型,提取需要的部分,这是构建复杂工具类型的基础。
1.5 预定义的条件类型工具
TypeScript 内置了几个非常实用的条件类型工具,理解它们的工作原理能帮助你更好地使用条件类型。
// Exclude<T, U>:从 T 中排除可赋值给 U 的类型
type Exclude<T, U> = T extends U ? never : T;
// Extract<T, U>:从 T 中提取可赋值给 U 的类型
type Extract<T, U> = T extends U ? T : never;
// NonNullable<T>:排除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// 测试
type Union = string | number | null | undefined;
type WithoutNull = NonNullable<Union>; // string | number
type OnlyString = Extract<Union, string>; // string
type NoString = Exclude<Union, string>; // number | null | undefined
二、映射类型:批量生成相似类型
2.1 映射类型的本质
当你需要基于一个现有类型创建一系列「相似但略有不同」的新类型时,映射类型是最佳选择。它通过遍历原始类型的键名,自动生成对应的属性类型。
// 最基础的映射类型
type Mapped<T> = {
[K in keyof T]: T[K];
};
// 将所有属性变为只读
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
// 将所有属性变为可选
type Partial<T> = {
[K in keyof T]?: T[K];
};
keyof T 获取 T 的所有键名(作为联合类型),[K in keyof T] 遍历每个键,T[K] 获取对应属性的类型。
2.2 键名映射与变换
映射类型不仅能保留原键名,还能对键名进行转换。
type User = {
id: number;
name: string;
email: string;
};
// 将所有键名加上 "new" 前缀
type NewUser = {
[K in keyof User as `new${Capitalize<string & K>}`]: User[K];
};
// 结果: { newId: number; newName: string; newEmail: string; }
// 将所有键名变为大写
type UppercaseUser = {
[K in keyof User as Uppercase<K>]: User[K];
};
// 结果: { ID: number; NAME: string; EMAIL: string; }
```
`as` 子句(TypeScript 4.1+ 引入)允许你对遍历到的键名进行重新映射或过滤。
### 2.3 使用映射类型过滤键
通过 `as` 子句结合条件类型,可以实现按条件筛选属性。
```typescript
type User = {
id: number;
name: string;
age: number;
privateKey: string;
};
// 只保留非 private 开头的属性
type PublicUser = {
[K in keyof User as K extends `private${string}` ? never : K]: User[K];
};
// 结果: { id: number; name: string; age: number; }
// 只保留字符串类型的属性
type StringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringOnlyUser = StringProps<User>;
// 结果: { name: string; }
2.4 映射类型修饰符
映射类型支持两个修饰符:readonly(用于属性)和 ?(用于可选性)。这两个修饰符既可以添加,也可以移除。
interface User {
id: number;
name: string;
}
// 添加 readonly 修饰符
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
// 移除 readonly 修饰符(原始类型需先有 readonly)
type Writable<T> = {
-readonly [K in keyof T]: T[K];
};
// 添加可选修饰符
type Optional<T> = {
[K in keyof T]?: T[K];
};
// 移除可选修饰符(原始类型需先有 ?)
type Required<T> = {
[K in keyof T]-?: T[K];
};
- 前缀表示移除修饰符,+ 前缀(显式写出时)表示添加修饰符。
三、组合实战:构建强大工具类型
3.1 实现 Pick 类型
Pick 用于从一个类型中挑选部分属性组成新类型。它完美展示了映射类型与条件类型的结合。
// 实现 Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
interface Todo {
id: number;
title: string;
completed: boolean;
description?: string;
}
type TodoPreview = MyPick<Todo, "id" | "title">;
// 结果: { id: number; title: string; }
关键点在于 K extends keyof T 这个约束,它确保你只能挑选实际存在的属性。
3.2 实现 Record 类型
Record 创建一个以给定类型为属性值的类型,常用于快速定义对象结构。
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
// 使用示例
type StatusMap = MyRecord<"pending" | "loading" | "success", boolean>;
// 结果: { pending: boolean; loading: boolean; success: boolean; }
3.3 实现复杂的结果类型转换
结合条件类型和映射类型,可以实现更复杂的类型转换逻辑。
interface APIResponse {
data: {
userId: number;
userName: string;
roles: string[];
};
status: number;
message: string;
}
// 将所有可选属性变为必填,所有必填属性变为可选
type FlipOptional<T> = {
[K in keyof T as T[K] extends Required<T>[K] ? never : K]: T[K];
} & {
[K in keyof T as T[K] extends Required<T>[K] ? K : never]?: T[K];
};
// 更实用的例子:将深层嵌套的对象属性全部变为可选
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface DeepNested {
a: {
b: {
c: string;
d: number;
};
};
e: string;
}
type PartialNested = DeepPartial<DeepNested>;
/* 结果:
{
a?: {
b?: {
c?: string;
d?: number;
};
};
e?: string;
}
*/
3.4 基于条件类型过滤联合类型成员
条件类型与映射类型结合,可以实现对联合类型成员的精确筛选。
type Union = string | number | boolean | (() => void) | symbol;
// 只保留函数类型
type FunctionMembers<T> = T extends (...args: any[]) => any ? T : never;
type OnlyFunctions = FunctionMembers<Union>;
// 结果: () => void
// 结合映射类型,将对象的所有函数属性提取出来
type FunctionProps<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
interface MyClass {
name: string;
save: () => void;
load: (data: string) => void;
id: number;
}
type MethodNames = FunctionProps<MyClass>;
// 结果: "save" | "load"
四、实际开发中的应用场景
4.1 API 响应类型处理
处理后端 API 返回数据时,响应结构通常包含 data、status、message 等字段,实际业务数据在 data 中。使用条件类型和映射类型可以优雅地定义这些类型关系。
interface APIResponse<T> {
code: number;
data: T;
msg: string;
timestamp: number;
}
// 提取 data 的类型(使用 ReturnType 的思想)
type ResponseData<T> = T extends { data: infer D } ? D : never;
// 处理异步函数的返回类型
type AsyncResult<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;
// 快速定义分页相关类型
type PageParams = {
page: number;
pageSize: number;
};
type PaginatedResponse<T> = {
list: T[];
total: number;
page: number;
pageSize: number;
};
4.2 状态管理中的类型安全
使用 TypeScript 的条件类型和映射类型,可以为状态管理提供完整的类型保障,确保所有状态变更都是类型安全的。
interface UserState {
user: {
id: number;
name: string;
email: string;
} | null;
loading: boolean;
error: string | null;
}
// 将所有属性变为只读
type ImmutableState<T> = {
readonly [K in keyof T]: T[K] extends object ? ImmutableState<T[K]> : T[K];
};
// 提取可 mutation 修改的属性(排除只读计算属性等)
type MutableKeys<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
4.3 事件处理类型安全
在事件系统中,映射类型可以帮助你基于事件名自动推导事件payload类型。
type EventMap = {
click: { x: number; y: number };
submit: { formData: Record<string, string> };
focus: null;
};
type EventPayload<K extends keyof EventMap> = EventMap[K];
// 将事件映射类型展开为联合类型
type Event<K extends keyof EventMap = keyof EventMap> = {
type: K;
payload: EventPayload<K>;
};
// 泛型事件类型,根据 type 自动推断 payload 类型
function emitEvent<K extends keyof EventMap>(event: Event<K>) {
// 事件派发逻辑
}
// TypeScript 会自动推断 payload 类型
emitEvent({ type: "click", payload: { x: 100, y: 200 } });
// emitEvent({ type: "click", payload: { wrong: "field" } }); // 报错
五、调试与最佳实践
5.1 类型推断调试
条件类型和映射类型嵌套后很难调试。TypeScript 提供了一个实用技巧:让类型计算「可视化」。
// 利用条件类型的分布式特性,将复杂类型展开
type Debug<T> = T extends any ? T : never;
// 使用交叉类型查看中间结果
type Intermediate = {
[K in keyof User]: [K, User[K]];
}[keyof User];
// Intermediate 变为 ["id", number] | ["name", string] 类型的联合
另一个实用技巧是用函数返回类型来触发类型推断:
type ShowType<T> = T extends infer U ? U : never;
// 使用时
type Visible = ShowType<YourComplexType>;
// 在 IDE 中 hover Visible 即可看到实际类型
5.2 性能考量
复杂的条件类型和映射类型可能导致类型计算时间变长,影响编译性能。建议遵循以下原则:
避免在热路径(如组件 props、频繁实例化的泛型类)上使用过于复杂的条件类型链。必要时分步定义中间类型,而非写成一个超长表达式。对于确定的类型关系,优先使用类型别名缓存结果,而非每次重新计算。
5.3 类型安全原则
始终为条件类型提供 else 分支(使用 never 而不是留空),确保所有可能的类型分支都被处理。对泛型参数添加必要的约束(extends),防止传入不合理的类型导致错误扩散。使用 keyof 和 extends 组合确保映射类型的键名安全,避免类型体操变成类型灾难。
条件类型和映射类型是 TypeScript 类型系统中最强大的两个特性。它们相互配合,可以构建出几乎任何你需要的类型结构。掌握它们的关键不是记忆语法,而是理解「类型也是值」这个核心思想——你可以像操作数据一样操作类型,条件判断、遍历映射、提取转换。当你能够熟练运用这些工具时,你会发现类型定义本身就是一种强大的文档和安全保障。

暂无评论,快来抢沙发吧!