Skip to content

枚举 ​

使用枚举我们可以定义一皮带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript支持数字的和基于字符串的枚举。

数字枚举 ​

首先我们看看数字枚举,如果你使用过其它编程语言应该会很熟悉。

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

如上,我们定义了一个数字枚举, Up使用初始化为 1。 其余的成员会从 1开始自动增长。 换句话说, Direction.Up的值为 1, Down为 2, Left为 3, Right为 4。

我们还可以完全不使用初始化器:

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

现在, Up的值为 0, Down的值为 1等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。

使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型:

ts
enum Response {
    No = 0,
    Yes = 1,
}

function respond(recipient: string, message: Response): void {
    // ...
}

respond("Princess Caroline", Response.Yes)

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

ts
enum E {
    A = getSomeValue(),
    B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}

字符串枚举 ​

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

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

虽然字符串枚举没有自动递增行为,但字符串枚举的优点是它们可以很好地“序列化”。换句话说,如果您正在调试并且必须读取数字枚举的运行时值,则该值通常是不透明的 - 它本身并不能传达任何有用的含义(尽管反向映射通常可以提供帮助)。字符串枚举允许您在代码运行时给出有意义且可读的值,而与枚举成员本身的名称无关。

异构枚举 ​

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

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

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

计算成员和常量成员 ​

每个枚举成员都有一个与其关联的值,该值可以是常量或计算值。如果满足以下条件,则枚举成员被视为常量:

  • 它是枚举中的第一个成员,并且没有初始值设定项,在这种情况下,它会被分配以下值0:

    ts
    // E.X is constant:
    enum E {
      X,
    }
  • 它没有初始值设定项,并且前面的枚举成员是数字常量。在这种情况下,当前枚举成员的值将是前一个枚举成员的值加一。

    ts
    // All enum members in 'E1' and 'E2' are constant.
     
    enum E1 {
      X,
      Y,
      Z,
    }
     
    enum E2 {
      A = 1,
      B,
      C,
    }
  • 枚举成员使用常量枚举表达式进行初始化。常量枚举表达式是 TypeScript 表达式的子集,可以在编译时完全求值。如果表达式满足以下条件,则该表达式是常量枚举表达式:

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

    将常量枚举表达式计算为NaNor是一个编译时错误Infinity。

在所有其他情况下,枚举成员被视为计算的。

ts
enum FileAccess {
  // constant members
  None,
  Read = 1 << 1,
  Write = 1 << 2,
  ReadWrite = Read | Write,
  // computed member
  G = "123".length,
}

联合枚举和枚举成员类型 ​

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

  • 任何字符串文字(例如"foo", "bar", "baz")
  • 任何数字文字(例如1, 100)
  • 应用于任何数字文字的一元减号(例如-1, -100)

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

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

ts
enum ShapeKind {
  Circle,
  Square,
}
 
interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}
 
interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}
 
let c: Circle = {
  kind: ShapeKind.Square,Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
  radius: 100,
};

另一个变化是枚举类型本身实际上成为每个枚举成员的联合。通过联合枚举,类型系统能够利用这样一个事实:它知道枚举本身存在的确切值集。因此,TypeScript 可以捕获我们可能错误地比较值的错误。例如:

ts
enum E {
  Foo,
  Bar,
}
 
function f(x: E) {
  if (x !== E.Foo || x !== E.Bar) {This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.
    //
  }
}

在该示例中,我们首先检查是否x不是。如果检查成功,那么我们的意志就会短路,“if”的主体就会运行。但是,如果检查没有成功,则只能是,因此查看它是否不等于是没有意义的。 E.Foo``||``x``E.Foo``E.Bar

运行时的枚举 ​

枚举是运行时存在的真实对象。例如,以下枚举

ts
enum E {
  X,
  Y,
  Z,
}

实际上可以传递给函数

ts
enum E {
  X,
  Y,
  Z,
}
 
function f(obj: { X: number }) {
  return obj.X;
}
 
// Works, since 'E' has a property named 'X' which is a number.
f(E);

编译时的枚举 ​

尽管枚举是运行时存在的真实对象,但该keyof关键字的工作方式与您对典型对象的预期不同。相反,使用keyof typeof获取将所有 Enum 键表示为字符串的 Type。

ts
enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
}
 
/**
 * This is equivalent to:
 * 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");

反向映射 ​

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

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

TypeScript 将其编译为以下 JavaScript:

ts
"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枚举上的修饰符定义的:

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

常量枚举只能使用常量枚举表达式,并且与常规枚举不同,它们在编译期间被完全删除。常量枚举成员在使用站点内联。这是可能的,因为常量枚举不能有计算成员。

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

在生成的代码中将变成

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

常量枚举陷阱 ​

内联枚举值一开始很简单,但具有微妙的含义。这些陷阱仅与环境常量枚举(基本上是.d.ts文件中的常量枚举)以及在项目之间共享它们有关,但如果您正在发布或使用.d.ts文件,这些陷阱可能适用于您,因为将文件tsc --declaration转换.ts为.d.ts文件。

  1. isolatedModules由于文档中列出的原因,该模式从根本上与环境常量枚举不兼容。这意味着如果您发布环境常量枚举,下游消费者将无法isolatedModules同时使用这些枚举值。
  2. 您可以在编译时轻松内联依赖项版本 A 中的值,并在运行时导入版本 B。如果您不太小心,版本 A 和 B 的枚举可以具有不同的值,从而导致令人惊讶的错误,例如采用错误的if语句分支。这些错误特别有害,因为通常在构建项目的同时运行自动化测试,并使用相同的依赖项版本,从而完全忽略了这些错误。
  3. importsNotUsedAsValues: "preserve"不会删除用作值的 const 枚举的导入,但环境 const 枚举不保证运行时.js文件存在。无法解析的导入会在运行时导致错误。目前,明确消除导入的常用方法(仅类型导入)不允许const enum 值。

以下是避免这些陷阱的两种方法:

  1. 根本不要使用常量枚举。您可以借助 linter轻松禁止常量枚举。显然,这避免了 const 枚举的任何问题,但会阻止您的项目内联自己的枚举。与内联其他项目的枚举不同,内联项目自己的枚举不会出现问题,并且会影响性能。

  2. 不要发布环境常量枚举,通过在preserveConstEnums. 这是TypeScript 项目本身内部采用的方法。 preserveConstEnumsconst 枚举与普通枚举发出相同的 JavaScript。然后,您可以在构建步骤中const安全地从.d.ts文件中删除修饰符。

    这样下游消费者就不会从您的项目中内联枚举,从而避免了上述陷阱,但项目仍然可以内联自己的枚举,这与完全禁止 const 枚举不同。

环境枚举 ​

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

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

环境枚举和非环境枚举之间的一个重要区别是,在常规枚举中,如果其前面的枚举成员被视为常量,则没有初始值设定项的成员将被视为常量。相比之下,没有初始化器的环境(和非常量)枚举成员始终被视为计算的。

对象与枚举 ​

as const在现代 TypeScript 中,当一个对象就足够时,您可能不需要枚举:

ts
const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
 
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;
 
EDirection.Up;
           
(enum member) EDirection.Up = 0
 
ODirection.Up;
           
(property) Up: 0
 
// Using the enum as a parameter
function walk(dir: EDirection) {}
 
// It requires an extra line to pull out the values
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);

与 TypeScript 相比,支持这种格式的最大论据enum是,它使您的代码库与 JavaScript 的状态保持一致,并且当/如果将枚举添加到 JavaScript 时,您可以转向附加语法。