Typescript 中的映射类型修饰符

2021-05-02

在2016年12月 Typescript 2.1 的时候,映射类型被添加到了这门语言中。作为 Typescript 2.8 的一部分,映射类型增加了给属性添加或删除特定修饰符的能力。在这之前,我们只能给属性添加修饰符,而不能移除它们。

? 属性修饰符

你可以在一个对象类型声明的任意属性后添加 ? 修饰符来使得该属性变成可选属性:

interface TodoItem {
  description: string;
  priority?: "high" | "medium" | "low";
}

添加了 ? 之后,在创建一个 TodoItem 类型的对象时可以设置也可以不设置 priority 属性:

// We can set the `priority` property to one of the 3 values
const todo1: TodoItem = {
  description: "Mow the lawn",
  priority: "high",
};

// Or we can leave it out entirely (since it's optional)
const todo2: TodoItem = {
  description: "Mow the lawn",
};

// Or we can explicitly set the value `undefined`
const todo3: TodoItem = {
  description: "Mow the lawn",
  priority: undefined,
};

我们已经知道如何将一个特定对象类型的特定属性变为可选属性。现在我们来看一看如何在泛型中运用 ? 修饰符来让一个给定类型的所有属性变成可选。

Partial 映射类型

转换一个给定类型的所有属性是映射类型典型的使用场景。一个映射类型定义了类型的转换程序。也就是,它可以得到一个已存在类型的所有属性,然后根据映射规则转换它们,最后创造一个由这些转换后属性组成的新类型。

让我们来定义一个 Partial<T> 映射类型,给泛型 T 的所有属性添加 ? 修饰符:

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

我们的 Partial<T> 类型使用了 keypf 操作符来得到 T 类型定义的所有属性。它同时使用了索引访问类型T[P]来查询T 的每一个 P 属性的类型。最后,通过 ?修饰符把每一个属性变成了可选。

如果我们将 Partial<T> 应用于前面的 TodoItem 类型,结果类型会得到两个可选的属性:

type PartialTodoItem = Partial<TodoItem>;
// {
//   description?: string | undefined;
//   priority?: "high" | "medium" | "low" | undefined;
// }

可以发现 Partial<T> 在很多应用中都非常有用,所以 Typescript 团队决定将它包括在 lib.es5.d.ts 文件中,作为 typescript 这个 npm 包的一部分一起发布。

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

移除 ?映射类型修饰符

我们已经知道如何通过Partial<T>给一个给定类型 T 的所有属性添加 ? 修饰符。那我们如何从一个给定类型的所有属性中移除 ? 修饰符呢?

在 Typescript 2.8 中,你可以给 ? 修饰符添加 - 前缀来移除它。一个移除了 ? 修饰符的属性会变成一个必需的属性。lib.es5.d.ts 文件现在包含了一个新定义的 Required<T> 类型来做这件事:

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

我们可以使用 Required<T> 来使得 TodoItem 类型的所有属性都变成必需:

type RequiredTodoItem = Required<TodoItem>;
// {
//   description: string;
//   priority: "high" | "medium" | "low";
// }

注意,在经过这个转换后,priority 属性不再是可选的。

添加 ? 映射类型修饰符

我们已经知道通过 -? 移除 ? 修饰符。为了保持对称和一致性,Typescript 允许你通过 +? 来给属性添加 ? 修饰符。所以你也可以像下面这样定义 Partial<T> 类型:

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

注意一个不包含 + 或者 - 前缀的属性修饰符和添加了 + 前缀的属性修饰符效果是一样的。写 +? 相比 ? 并没有什么额外的好处。我建议你还是继续使用 ?,它是你在一个接口或类型别名中定义可选属性所使用的语法。

readonly 属性修饰符

你可以在映射类型中使用 readonly 修饰符来使得属性变成只读:

type ReadonlyTodoItem = Readonly<TodoItem>;
// {
//   readonly description?: string | undefined;
//   readonly priority?: "high" | "medium" | "low" | undefined;
// }

如果你给一个只读属性赋值编译器会提示错误:

const todo: ReadonlyTodoItem = {
  description: "Mow the lawn",
  priority: "high",
};

// Error: Cannot assign to 'priority'
// because it is a read-only property.
todo.priority = "medium";

移除 readonly 映射类型修饰符

和通过 -? 移除 ? 修饰符类似,你可以通过 -readonly 移除 readonly 修饰符。让我们来定义一个 Mutable<T> 类型,帮助我们移除 T 类型中的所有属性的 readonly 修饰符:

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

现在,下面的代码变成类型正确了,编译器不会再抱怨说不能给一个只读属性赋值:

const todo: Mutable<ReadonlyTodoItem> = {
  description: "Mow the lawn",
  priority: "high",
};

todo.priority = "medium";

添加 readyonly 映射类型修饰符

和通过 +? 给属性添加 ? 修饰符类似,你也可以通过 +readonly 给属性添加 readonly 修饰符。所以你可以像下面这样重写预定义的 Readonly<T> 映射类型:

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

再一次,我建议你继续使用普通的 readonly,因为使用 +readonly 并没有什么额外的好处。

郑超的独立博客