深入理解 TypeScript
请支持本书:购买捐赠
(广告,请不要屏蔽。)

12 TypeScript 枚举:它们如何工作?它们可以用来做什么?



本章回答以下两个问题

下一章 中,我们将介绍枚举的替代方案。

12.1 基础知识

boolean 是一种具有有限数量值的类型:falsetrue。使用枚举,TypeScript 允许我们自己定义类似的类型。

12.1.1 数字枚举

这是一个数字枚举

enum NoYes {
  No = 0,
  Yes = 1, // trailing comma
}

assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

说明

我们可以像使用 true123'abc' 等字面量一样使用成员,例如

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}
assert.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja');

12.1.2 基于字符串的枚举

我们也可以使用字符串作为枚举成员值,而不是数字

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');

12.1.3 异构枚举

最后一种枚举称为_异构枚举_。异构枚举的成员值是数字和字符串的混合

enum Enum {
  One = 'One',
  Two = 'Two',
  Three = 3,
  Four = 4,
}
assert.deepEqual(
  [Enum.One, Enum.Two, Enum.Three, Enum.Four],
  ['One', 'Two', 3, 4]
);

异构枚举不常用,因为它们的应用很少。

遗憾的是,TypeScript 仅支持数字和字符串作为枚举成员值。其他值,例如符号,是不允许的。

12.1.4 省略初始化器

我们可以在两种情况下省略初始化器

这是一个没有任何初始化器的数字枚举

enum NoYes {
  No,
  Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);

这是一个省略了一些初始化器的异构枚举

enum Enum {
  A,
  B,
  C = 'C',
  D = 'D',
  E = 8, // (A)
  F,
}
assert.deepEqual(
  [Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
  [0, 1, 'C', 'D', 8, 9]
);

请注意,我们不能省略 A 行中的初始化器,因为前一个成员的值不是数字。

12.1.5 枚举成员名称的大小写

命名常量(在枚举或其他地方)有几种先例

12.1.6 枚举成员名称的引用

与 JavaScript 对象类似,我们可以引用枚举成员的名称

enum HttpRequestField {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);

无法计算枚举成员的名称。对象字面量通过方括号支持计算属性键。

12.2 指定枚举成员值(高级)

TypeScript 根据枚举成员的初始化方式将其分为三种

到目前为止,我们只使用了字面量成员。

在前面的列表中,较早提到的成员灵活性较低,但支持更多功能。继续阅读以获取更多信息。

12.2.1 字面量枚举成员

如果枚举成员的值是指定的,则该成员是_字面量_

如果枚举只有字面量成员,我们可以将这些成员用作类型(类似于例如数字字面量可以用作类型)

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(x: NoYes.No) { // (A)
  return x;
}

func(NoYes.No); // OK

// @ts-expect-error: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');

// @ts-expect-error: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);

A 行中的 NoYes.No 是_枚举成员类型_。

此外,字面量枚举支持穷尽性检查(我们将在稍后介绍)。

12.2.2 常量枚举成员

如果枚举成员的值可以在编译时计算,则该成员是常量。因此,我们可以隐式指定其值(即,我们让 TypeScript 为我们指定)。或者我们可以显式指定它,并且只允许使用以下语法

这是一个其成员都是常量的枚举示例(我们将在后面看到该枚举是如何使用的)

enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

通常,常量成员不能用作类型。但是,仍然会执行穷尽性检查。

12.2.3 计算枚举成员

_计算枚举成员_的值可以通过任意表达式指定。例如

enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}

这是一个数字枚举。基于字符串的枚举和异构枚举的限制更多。例如,我们不能使用方法调用来指定成员值

enum NoYesStr {
  No = 'No',
  // @ts-expect-error: Computed values are not permitted in
  // an enum with string valued members.
  Yes = ['Y', 'e', 's'].join(''),
}

TypeScript 不对计算枚举成员执行穷尽性检查。

12.3 数字枚举的缺点

12.3.1 缺点:日志记录

记录数字枚举的成员时,我们只看到数字

enum NoYes { No, Yes }

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 0
// 1

12.3.2 缺点:松散的类型检查

使用枚举作为类型时,静态允许的值不仅是枚举成员的值,还接受任何数字

enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); // no error!

为什么没有更严格的静态检查?Daniel Rosenwasser 解释说

这种行为是由按位运算引起的。有时 SomeFlag.Foo | SomeFlag.Bar 旨在生成另一个 SomeFlag。但最终得到的是 number,而您不想将其转换回 SomeFlag

我认为如果我们重新设计 TypeScript 并且仍然有枚举,我们会为位标志创建一个单独的结构。

稍后将更详细地演示如何将枚举用于位模式。

12.3.3 建议:优先使用基于字符串的枚举

我的建议是优先使用基于字符串的枚举(为了简洁起见,本章并不总是遵循此建议)

enum NoYes { No='No', Yes='Yes' }

一方面,日志输出对人类更有用

console.log(NoYes.No);
console.log(NoYes.Yes);

// Output:
// 'No'
// 'Yes'

另一方面,我们获得了更严格的类型检查

function func(noYes: NoYes) {}

// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');

// @ts-expect-error: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes'); // (A)

甚至不允许使用等于成员值的字符串(A 行)。

12.4 枚举的用例

12.4.1 用例:位模式

Node.js 文件系统模块 中,有几个函数具有参数 mode。它通过从 Unix 继承的数字编码指定文件权限

这意味着权限可以用 9 位表示(3 个类别,每个类别 3 个权限)

用户 所有
权限 r、w、x r、w、x r、w、x
8, 7, 6 5, 4, 3 2, 1, 0

Node.js 没有这样做,但我们可以使用枚举来处理这些标志

enum Perm {
  UserRead     = 1 << 8, // bit 8
  UserWrite    = 1 << 7,
  UserExecute  = 1 << 6,
  GroupRead    = 1 << 5,
  GroupWrite   = 1 << 4,
  GroupExecute = 1 << 3,
  AllRead      = 1 << 2,
  AllWrite     = 1 << 1,
  AllExecute   = 1 << 0,
}

位模式通过 按位或 组合

// User can change, read and execute.
// Everyone else can only read and execute.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
  Perm.GroupRead | Perm.GroupExecute |
  Perm.AllRead | Perm.AllExecute,
  0o755);

// User can read and write.
// Group members can read.
// Everyone can’t access at all.
assert.equal(
  Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
  0o640);
12.4.1.1 位模式的替代方案

位模式背后的主要思想是,有一组标志,并且可以选择这些标志的任何子集。

因此,使用真正的集合来选择子集是执行相同任务的更直接的方式

enum Perm {
  UserRead = 'UserRead',
  UserWrite = 'UserWrite',
  UserExecute = 'UserExecute',
  GroupRead = 'GroupRead',
  GroupWrite = 'GroupWrite',
  GroupExecute = 'GroupExecute',
  AllRead = 'AllRead',
  AllWrite = 'AllWrite',
  AllExecute = 'AllExecute',
}
function writeFileSync(
  thePath: string, permissions: Set<Perm>, content: string) {
  // ···
}
writeFileSync(
  '/tmp/hello.txt',
  new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
  'Hello!');

12.4.2 用例:多个常量

有时,我们有一些属于一起的常量集

const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');

这是一个很好的枚举用例

enum LogLevel {
  off = 'off',
  info = 'info',
  warn = 'warn',
  error = 'error',
}

枚举的一个好处是常量名称被分组并嵌套在命名空间 LogLevel 中。

另一个好处是我们自动为它们获取类型 LogLevel。如果我们想要为常量使用这种类型,则需要做更多的工作

type LogLevel =
  | typeof off
  | typeof info
  | typeof warn
  | typeof error
;

有关此方法的更多信息,请参阅 §13.1.3 “符号单例类型的联合”

12.4.3 用例:比布尔值更具描述性

当使用布尔值表示备选项时,枚举通常更具描述性。

12.4.3.1 类似布尔值的示例:有序列表与无序列表

例如,要表示列表是否有序,我们可以使用布尔值

class List1 {
  isOrdered: boolean;
  // ···
}

但是,枚举更具描述性,并且具有如果需要,我们可以在以后添加更多备选项的额外好处。

enum ListKind { ordered, unordered }
class List2 {
  listKind: ListKind;
  // ···
}
12.4.3.2 类似布尔值的示例:错误处理模式

同样,我们可以通过布尔值指定如何处理错误

function convertToHtml1(markdown: string, throwOnError: boolean) {
  // ···
}

或者我们可以通过枚举值来做到这一点

enum ErrorHandling {
  throwOnError = 'throwOnError',
  showErrorsInContent = 'showErrorsInContent',
}
function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
  // ···
}

12.4.4 用例:更好的字符串常量

考虑以下创建正则表达式的函数。

const GLOBAL = 'g';
const NOT_GLOBAL = '';
type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;

function createRegExp(source: string,
  globalness: Globalness = NOT_GLOBAL) {
    return new RegExp(source, 'u' + globalness);
  }

assert.deepEqual(
  createRegExp('abc', GLOBAL),
  /abc/ug);

assert.deepEqual(
  createRegExp('abc', 'g'), // OK
  /abc/ug);

我们可以使用枚举来代替字符串常量

enum Globalness {
  Global = 'g',
  notGlobal = '',
}

function createRegExp(source: string, globalness = Globalness.notGlobal) {
  return new RegExp(source, 'u' + globalness);
}

assert.deepEqual(
  createRegExp('abc', Globalness.Global),
  /abc/ug);

assert.deepEqual(
  // @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Globalness | undefined'. (2345)
  createRegExp('abc', 'g'), // error
  /abc/ug);

这种方法有什么好处?

12.5 运行时的枚举

TypeScript 将枚举编译为 JavaScript 对象。例如,以下枚举

enum NoYes {
  No,
  Yes,
}

TypeScript 将此枚举编译为

var NoYes;
(function (NoYes) {
  NoYes[NoYes["No"] = 0] = "No";
  NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));

在此代码中,进行了以下赋值

NoYes["No"] = 0;
NoYes["Yes"] = 1;

NoYes[0] = "No";
NoYes[1] = "Yes";

有两组赋值

12.5.1 反向映射

给定一个数字枚举

enum NoYes {
  No,
  Yes,
}

正常映射是从成员名称到成员值

// Static (= fixed) lookup:
assert.equal(NoYes.Yes, 1);

// Dynamic lookup:
assert.equal(NoYes['Yes'], 1);

数字枚举还支持从成员值到成员名称的_反向映射_

assert.equal(NoYes[1], 'Yes');

反向映射的一个用例是打印枚举成员的名称。

function getQualifiedName(value: NoYes) {
  return 'NoYes.' + NoYes[value];
}
assert.equal(
  getQualifiedName(NoYes.Yes), 'NoYes.Yes');

12.5.2 运行时的字符串枚举

字符串枚举在运行时具有更简单的表示。

考虑以下枚举。

enum NoYes {
  No = 'NO!',
  Yes = 'YES!',
}

它被编译成以下 JavaScript 代码。

var NoYes;
(function (NoYes) {
    NoYes["No"] = "NO!";
    NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));

TypeScript 不支持字符串枚举的反向映射。

12.6 const 枚举

如果枚举以关键字 const 为前缀,则它在运行时没有表示形式。相反,直接使用其成员的值。

12.6.1 编译非 const 枚举

为了观察这种效果,让我们首先检查以下非 const 枚举。

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}

function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

TypeScript 将此代码编译为:

"use strict";
var NoYes;
(function (NoYes) {
  NoYes["No"] = "No";
  NoYes["Yes"] = "Yes";
})(NoYes || (NoYes = {}));

function toGerman(value) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

12.6.2 编译 const 枚举

这与之前的代码相同,但现在枚举是 const。

const enum NoYes {
  No,
  Yes,
}
function toGerman(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
  }
}

现在,枚举作为构造函数的表示形式消失了,只剩下其成员的值。

function toGerman(value) {
  switch (value) {
    case "No" /* No */:
      return 'Nein';
    case "Yes" /* Yes */:
      return 'Ja';
  }
}

12.7 编译时的枚举

12.7.1 枚举是对象

TypeScript 将(非 const)枚举视为对象。

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
function func(obj: { No: string }) {
  return obj.No;
}
assert.equal(
  func(NoYes), // allowed statically!
  'No');

12.7.2 字面量枚举的安全检查

当我们接受枚举成员值时,我们通常要确保:

继续阅读以获取更多信息。我们将使用以下枚举。

enum NoYes {
  No = 'No',
  Yes = 'Yes',
}
12.7.2.1 防止非法值

在以下代码中,我们采取了两项措施来防止非法值。

function toGerman1(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new TypeError('Unsupported value: ' + JSON.stringify(value));
  }
}

assert.throws(
  // @ts-expect-error: Argument of type '"Maybe"' is not assignable to
  // parameter of type 'NoYes'.
  () => toGerman1('Maybe'),
  /^TypeError: Unsupported value: "Maybe"$/);

这些措施是:

12.7.2.2 通过穷举检查防止忘记 case

我们可以采取一项措施。以下代码执行_穷举检查_:如果我们忘记考虑所有枚举成员,TypeScript 会向我们发出警告。

class UnsupportedValueError extends Error {
  constructor(value: never) {
    super('Unsupported value: ' + value);
  }
}

function toGerman2(value: NoYes) {
  switch (value) {
    case NoYes.No:
      return 'Nein';
    case NoYes.Yes:
      return 'Ja';
    default:
      throw new UnsupportedValueError(value);
  }
}

穷举检查是如何工作的?对于每种情况,TypeScript 都会推断 value 的类型。

function toGerman2b(value: NoYes) {
  switch (value) {
    case NoYes.No:
      // %inferred-type: NoYes.No
      value;
      return 'Nein';
    case NoYes.Yes:
      // %inferred-type: NoYes.Yes
      value;
      return 'Ja';
    default:
      // %inferred-type: never
      value;
      throw new UnsupportedValueError(value);
  }
}

在默认情况下,TypeScript 会将 value 的类型推断为 never,因为我们永远不会到达那里。但是,如果我们向 NoYes 添加一个成员 .Maybe,则 value 的推断类型为 NoYes.Maybe。并且该类型在静态上与 new UnsupportedValueError() 的参数类型 never 不兼容。这就是为什么我们在编译时会收到以下错误消息。

Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.

方便的是,这种穷举检查也适用于 if 语句。

function toGerman3(value: NoYes) {
  if (value === NoYes.No) {
    return 'Nein';
  } else if (value === NoYes.Yes) {
    return 'Ja';
  } else {
    throw new UnsupportedValueError(value);
  }
}
12.7.2.3 另一种检查穷举性的方法

或者,如果我们指定了返回类型,我们也会得到穷举性检查。

function toGerman4(value: NoYes): string {
  switch (value) {
    case NoYes.No:
      const x: NoYes.No = value;
      return 'Nein';
    case NoYes.Yes:
      const y: NoYes.Yes = value;
      return 'Ja';
  }
}

如果我们向 NoYes 添加一个成员,则 TypeScript 会抱怨 toGerman4() 可能会返回 undefined

这种方法的缺点

12.7.3 keyof 和枚举

我们可以使用 keyof 类型运算符来创建其元素为枚举成员键的类型。当我们这样做时,我们需要将 keyoftypeof 结合起来。

enum HttpRequestKeyEnum {
  'Accept',
  'Accept-Charset',
  'Accept-Datetime',
  'Accept-Encoding',
  'Accept-Language',
}
// %inferred-type: "Accept" | "Accept-Charset" | "Accept-Datetime" |
// "Accept-Encoding" | "Accept-Language"
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;

function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
  // ···
}
12.7.3.1 不带 typeof 使用 keyof

如果我们在不使用 typeof 的情况下使用 keyof,我们会得到一个不同的、不太有用的类型。

// %inferred-type: "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Keys = keyof HttpRequestKeyEnum;

keyof HttpRequestKeyEnumkeyof number 相同。

12.8 致谢