TypeScript 映射类型详解及应用(二十一)

在现代前端开发中,TypeScript 以其强大的类型系统逐渐成为主流选择。映射类型(Mapped Types)作为 TypeScript 类型系统中的一个重要特性,能够帮助开发者高效地创建和管理复杂类型。本文将详细介绍映射类型的基本概念、常见用法以及实际应用场景,帮助你在项目中更好地利用这一强大工具。

什么是映射类型?

映射类型是一种基于现有类型创建新类型的机制。通过遍历已有类型的所有属性,并对每个属性应用相同的类型转换规则,我们可以快速生成新的类型。这种声明式的类型生成方式不仅减少了手动定义类型的工作量,还提高了代码的可维护性和灵活性。

为什么需要映射类型?

在实际开发中,我们经常需要基于现有的类型创建一些变体。例如,将所有属性变为可选的类型、将所有属性变为只读的类型等。传统的做法是手动定义这些类型,但这不仅繁琐且容易出错。映射类型提供了一种简洁而强大的方式来自动化这一过程。

基础映射类型

基础映射类型通过遍历原始类型的所有属性来创建新的类型。这是实现 Partial、Readonly 等内置工具类型的基础。

示例

假设我们有一个 User 接口:

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

我们可以使用映射类型将其所有属性变为可选:

type PartialUser = {
  [P in keyof User]?: User[P];
};

结果类型为:

type PartialUser = {
  id?: number;
  name?: string;
};

语法解释

  • [P in keyof T]:遍历 T 类型的所有键。
  • ?:将属性设为可选。

属性修饰符

映射类型支持多种属性修饰符,这些修饰符可以组合使用,实现不同的类型转换需求。

可选修饰符

通过在属性名前添加 ? 修饰符,可以将属性设为可选:

type Partial
<T> = {
  [P in keyof T]?: T[P];
};

只读修饰符

通过在属性名前添加 readonly 修饰符,可以将属性设为只读:

type Readonly
<T> = {
  readonly [P in keyof T]: T[P];
};

移除可选修饰符

通过在属性名前添加 -? 修饰符,可以移除原有的可选修饰符:

type Required
<T> = {
  [P in keyof T]-?: T[P];
};

示例

interface User {
  name: string;
  age: number;
}

const readonlyUser: Readonly
<User> = {
  name: "Alice",
  age: 25,
};

// readonlyUser.age = 30; // 错误:只读属性不能修改

const optionalUser: Partial
<User> = {
  name: "Bob",
};

console.log("只读: " + JSON.stringify(readonlyUser));
console.log("可选: " + JSON.stringify(optionalUser));

键名映射

映射类型还可以使用 as 关键字来重映射键名。这在需要统一修改键名格式时非常有用。

示例

假设我们有一个 User 接口:

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

我们可以使用 as 关键字为所有键添加前缀:

type WithPrefix<T, Prefix extends string> = {
  [P in keyof T as `${Prefix}${Capitalize<string & P>}`]: T[P];
};

type PrefixedUser = WithPrefix<User, "user">;

const user: PrefixedUser = {
  userId: 1,
  userName: "Alice",
  userAge: 25,
};

console.log("带前缀: " + JSON.stringify(user));

模板字面量类型

使用 ${Prefix}${Capitalize} 可以动态生成新的键名。Capitalize 用于将字符串的首字母大写。

键过滤

通过条件类型和映射类型的组合,可以实现键的过滤。这在实现 Omit 等工具类型时非常有用。

示例

假设我们有一个 User 接口:

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

我们可以使用条件类型过滤掉指定的键:

type Omit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

type UserWithoutPassword = Omit<User, "password">;

const user: UserWithoutPassword = {
  id: 1,
  name: "Alice",
  email: "a@b.com",
};

console.log("无密码: " + JSON.stringify(user));

never 类型

在映射类型中使用 never 作为属性类型,该属性会被完全移除。

条件映射

映射类型可以与条件类型结合,根据属性类型的不同应用不同的转换。这使得类型转换更加灵活和智能。

示例

假设我们有一个 APIResponse 接口:

interface APIResponse {
  data: string;
  error: string;
  isLoading: boolean;
  timestamp: number;
}

我们可以将函数类型转换为 () => void:

type FunctionToVoid
<T> = {

  [P in keyof T]: T[P] extends (...args: any[]) => any ? () => void : T[P];
};

const response: FunctionToVoid
<APIResponse> = {
  data: "hello",
  error: "",
  isLoading: false,
  timestamp: Date.now(),
};

console.log("响应: " + JSON.stringify(response));

应用场景

条件映射常用于处理 API 响应、清理配置对象等需要根据类型做不同处理的场景。

内置映射类型

TypeScript 内置了许多基于映射类型实现的工具类型,这些工具类型可以满足大多数日常开发需求。

示例

// Partial - 将所有属性变为可选
type P1 = Partial<{ a: string; b: number }>; // { a?: string; b?: number }

// Required - 将所有可选属性变为必填
type R1 = Required<{ a?: string; b?: number }>; // { a: string; b: number }

// Readonly - 将所有属性变为只读
type RO1 = Readonly<{ a: string; b: number }>; // { readonly a: string; readonly b: number }

// Pick - 选择指定的属性
type PK = Pick<{ a: string; b: number; c: boolean }, "a" | "b">; // { a: string; b: number }

// Omit - 排除指定的属性
type OM = Omit<{ a: string; b: number; c: boolean }, "c">; // { a: string; b: number }

console.log("Partial: " + JSON.stringify({} as P1));
console.log("Pick: " + JSON.stringify({ a: "x" } as PK));

工具类型组合

这些内置工具类型都是基于映射类型和条件类型实现的。了解其原理可以更好地使用它们。

注意事项

  • keyof 关键字: 用于获取类型的所有键组成的联合类型。
  • in 关键字: 用于遍历键名联合类型。
  • 修饰符位置: ? 和 readonly 在属性名前,表示添加修饰符。
  • 减号修饰符: -? 和 -readonly 用于移除修饰符。
  • as 关键字: 用于重映射键名,必须返回字符串或数字字面量类型。

进阶

映射类型可以与条件类型、模板字面量类型组合,实现复杂的类型转换。

总结

映射类型是 TypeScript 类型系统中最强大的特性之一。通过 keyof 获取类型的所有键,使用 in 遍历键名进行映射,添加 ? 和 readonly 修饰符,使用 as 重映射键名,映射类型提供了丰富的功能来简化类型定义和管理。

  • keyof: 获取类型的所有键
  • in: 遍历键名进行映射
  • ?: 添加可选修饰符
  • readonly: 添加只读修饰符
  • -?: 移除可选修饰符
  • as: 重映射键名

善用映射类型可以大幅减少重复的类型定义,提高代码的可维护性。希望本文能帮助你在 TypeScript 开发中更好地利用这一强大工具。