Skip to content
0

TypeScript Office - 枚举

枚举

Enums 是 TypeScript 的少数功能之一,它不是 JavaScript 的类型级扩展。

枚举允许开发者定义一组命名的常量。使用枚举可以使其更容易记录意图,或创建一组不同的情况。TypeScript 提供了基于数字和字符串的枚举。

数值型枚举

我们首先从数字枚举开始,如果你来自其他语言,可能会更熟悉它。一个枚举可以用 enum 关键字来定义。

typescript
enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

上面,我们有一个数字枚举,其中 Up 被初始化为 1,所有下面的成员从这一点开始自动递增。换句话说,Direction.Up 的值是 1,Down 是 2,Left 是 3,Right 是 4。

如果我们愿意,我们可以完全不使用初始化器:

typescript
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

这里,Up 的值是 0,Down 是 1,依次类推。这种自动递增的行为对于我们可能不关心成员值本身,但关心每个值与同一枚举中的其他值不同的情况很有用。

使用枚举很简单:只需将任何成员作为枚举本身的一个属性来访问,并使用枚举的名称来声明类型:

typescript
enum UserResponse {
  No = 0,
  Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
  // ...
}
respond("Princess Caroline", UserResponse.Yes);

数字枚举可以混合在计算和常量成员中(见下文)。简而言之,没有初始化器的枚举要么需要放在第一位,要么必须放在用数字常量或其他常量枚举成员初始化的数字枚举之后。换句话说,下面的情况是不允许的:

typescript
enum E {
  A = getSomeValue(),
  B,
  // Ⓧ Enum 成员必须有初始化器。
}

字符串枚举

字符串枚举是一个类似的概念,但有一些细微的运行时差异,如下文所述。在一个字符串枚举中,每个成员都必须用一个字符串字头或另一个字符串枚举成员进行常量初始化。

typescript
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

虽然字符串枚举没有自动递增的行为,但字符串枚举有一个好处,那就是它们可以很好地「序列化」。换句话说,如果你在调试时不得不读取一个数字枚举的运行时值,这个值往往是不透明的,它本身并不传达任何有用的意义(尽管反向映射往往可以帮助你),字符串枚举允许你在代码运行时给出一个有意义的、可读的值,与枚举成员本身的名称无关。

异构枚举

从技术上讲,枚举可以与字符串和数字成员混合,但不清楚为什么你会想这样做:

typescript
enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

除非你真的想以一种巧妙的方式利用 JavaScript 的运行时行为,否则建议你不要这样做。

计算型和常量型成员

每个枚举成员都有一个与之相关的值,可以是常量,也可以是计算值。一个枚举成员被认为是常数,如果:

  • 它是枚举中的第一个成员,它没有初始化器,在这种情况下,它被赋值为 0:
typescript
// E.X is constant:
enum E {
  X,
}
  • 它没有一个初始化器,而且前面的枚举成员是一个数字常数。在这种情况下,当前枚举成员的值将是前一个枚举成员的值加 1:
typescript
// 'E1'和'E2'中的所有枚举成员都是常数。
enum E1 {
  X,
  Y,
  Z,
}
enum E2 {
  A = 1,
  B,
  C,
}

枚举成员用一个常量枚举表达式进行初始化。常量枚举表达式是 TypeScript 表达式的一个子集,可以在编译时进行完全评估。一个表达式是一个常量枚举表达式,如果它是:

  1. 枚举表达式的字面意思(基本上是一个字符串字面量或一个数字字面量)
  2. 对先前定义的常量枚举成员的引用(可以来自不同的枚举)
  3. 一个括号内的常量枚举表达式
  4. 应用于常量枚举表达式的 +-~ 单项运算符之一
  5. +-*/%<<>>>>&|^ 以常量枚举表达式为操作数的二元运算符

如果常量枚举表达式被评估为 NaN 或 Infinity,这是一个编译时错误。

在所有其他情况下,枚举成员被认为是计算出来的。

typescript
enum FileAccess {
  // 常量成员
  None,
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write,
  // 计算成员
  G = "123".length,
}

联合枚举和枚举成员类型

有一个特殊的常量枚举成员的子集没有被计算:字面枚举成员。字面枚举成员是一个没有初始化值的常量枚举成员,或者其值被初始化为:

  • 任何字符串(例如:"foo","bar","baz")
  • 任何数字字头(例如:1,100)
  • 应用于任何数字字面的单数减号(例如:-1,-100)

当一个枚举中的所有成员都有枚举的字面价值时,一些特殊的语义就会发挥作用。

首先,枚举成员也成为了类型。例如,我们可以说某些成员只能有一个枚举成员的值:

typescript
enum ShapeKind {
  Circle,
  Square,
}
interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}
interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}
let c: Circle = {
  kind: ShapeKind.Square,
  // Ⓧ 类型 'ShapeKind.Square' 不能被分配给类型 'ShapeKind.Circle'
  radius: 100,
};

另一个变化是枚举类型本身有效地成为每个枚举成员的联盟。通过联合枚举,类型系统能够利用这一事实,即它知道存在于枚举本身的精确的值集。正因为如此,TypeScript 可以捕捉到我们可能错误地比较数值的错误。比如说:

typescript
enum E {
  Foo,
  Bar,
}
function f(x: E) {
  if (x !== E.Foo || x !== E.Bar) {
    // Ⓧ 这个条件将总是返回'true',因为'E.Foo'和'E.Bar'的类型没有重合。
    //...
  }
}

在这个例子中,我们首先检查了 x 是否不是 E.Foo。如果这个检查成功了,那么我们的 || 就会短路,if 的主体就会运行。然而,如果检查没有成功,那么 x 就只能是 E.Foo,所以看它是否等于 E.Bar 就没有意义了。

运行时的枚举

枚举是在运行时存在的真实对象。例如,下面这个枚举:

typescript
enum E {
  X,
  Y,
  Z,
}

实际上可以被传递给函数:

typescript
enum E {
  X,
  Y,
  Z,
}
function f(obj: { X: number }) {
  return obj.X;
}
// 可以正常工作,因为'E'有一个名为'X'的属性,是一个数字。
f(E);

编译时的枚举

尽管 Enum 是在运行时存在的真实对象,keyof 关键字的工作方式与你对典型对象的预期不同。相反,使用 keyof、typeof 来获得一个将所有 Enum 键表示为字符串的类型。

typescript
enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
}
/**
 * 这相当于:
 * type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
 */
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
  const num = LogLevel[key];
  if (num <= LogLevel.WARN) {
    console.log("Log level key is:", key);
    console.log("Log level value is:", num);
    console.log("Log level message is:", message);
  }
}
printImportant("ERROR", "This is a message");
  • 反向映射

除了为成员创建一个带有属性名称的对象外,数字枚举的成员还可以得到从枚举值到枚举名称的反向映射。例如,在这个例子中:

typescript
enum Enum {
  A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

TypeScript 将其编译为以下的 JavaScript:

typescript
"use strict";
var Enum;
(function (Enum) {
  Enum[(Enum["A"] = 0)] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

在这段生成的代码中,一个枚举被编译成一个对象,它同时存储了正向 (name -> value)和反向 (value -> name) 的映射关系。对其他枚举成员的引用总是以属性访问的方式发出,而且从不内联。

请记住,字符串枚举成员根本不会被生成反向映射。

  • const 枚举

在大多数情况下,枚举是一个完全有效的解决方案。然而有时要求比较严格。为了避免在访问枚举值时支付额外的生成代码和额外的间接性的代价,可以使用 const 枚举。常量枚举是使用我们枚举上的 const 修饰符来定义的。

typescript
const enum Enum {
  A = 1,
  B = A * 2,
}

常量枚举只能使用常量枚举表达式,与普通枚举不同,它们在编译过程中被完全删除。常量枚举成员在使用地点被内联。这是可能的,因为常量枚举不能有计算的成员。

typescript
const enum Direction {
  Up,
  Down,
  Left,
  Right,
}
let directions = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];

在生成的代码中,将变成:

typescript
"use strict";
let directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

环境枚举

环境枚举是用来描述已经存在的枚举类型的形状。

typescript
declare enum Enum {
  A = 1,
  B,
  C = 2,
}

环境枚举和非环境枚举之间的一个重要区别是,在常规枚举中,如果其前面的枚举成员被认为是常量,那么没有初始化器的成员将被认为是常量。相反,一个没有初始化器的环境(和非常量)枚举成员总是被认为是计算的。

对象与枚举

在现代 TypeScript 中,你可能不需要一个枚举,因为一个对象的常量就足够了:

typescript
const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;
// (enum member) EDirection.Up = 0
EDirection.Up;
// (property) Up: 0
ODirection.Up;
// 将枚举作为一个参数
function walk(dir: EDirection) {}
// 它需要一个额外的行来拉出数值
type Direction = (typeof ODirection)[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right);

与 TypeScript 的枚举相比,支持这种格式的最大理由是,它使你的代码库与 JavaScript 的状态保持一致,when/if 枚举被添加到 JavaScript 中,那么你可以转移到额外的语法。

最近更新