枚举

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

¥Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript.

枚举允许开发者定义一组命名常量。使用枚举可以更轻松地记录意图,或创建一组不同的案例。TypeScript 提供基于数字和基于字符串的枚举。

¥Enums allow a developer to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums.

数字枚举

¥Numeric enums

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

¥We’ll first start off with numeric enums, which are probably more familiar if you’re coming from other languages. An enum can be defined using the enum keyword.

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

上面,我们有一个数字枚举,其中 Up1 初始化。从那时起,以下所有成员都会自动递增。换句话说,Direction.Up 的值为 1Down 的值为 2Left 的值为 3Right 的值为 4

¥Above, we have a numeric enum where Up is initialized with 1. All of the following members are auto-incremented from that point on. In other words, Direction.Up has the value 1, Down has 2, Left has 3, and Right has 4.

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

¥If we wanted, we could leave off the initializers entirely:

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

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

¥Here, Up would have the value 0, Down would have 1, etc. This auto-incrementing behavior is useful for cases where we might not care about the member values themselves, but do care that each value is distinct from other values in the same enum.

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

¥Using an enum is simple: just access any member as a property off of the enum itself, and declare types using the name of the enum:

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

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

¥Numeric enums can be mixed in computed and constant members (see below). The short story is, enums without initializers either need to be first, or have to come after numeric enums initialized with numeric constants or other constant enum members. In other words, the following isn’t allowed:

ts
enum E {
A = getSomeValue(),
B,
Enum member must have initializer.1061Enum member must have initializer.
}
Try

字符串枚举

¥String enums

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

¥String enums are a similar concept, but have some subtle runtime differences as documented below. In a string enum, each member has to be constant-initialized with a string literal, or with another string enum member.

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

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

¥While string enums don’t have auto-incrementing behavior, string enums have the benefit that they “serialize” well. In other words, if you were debugging and had to read the runtime value of a numeric enum, the value is often opaque - it doesn’t convey any useful meaning on its own (though reverse mapping can often help). String enums allow you to give a meaningful and readable value when your code runs, independent of the name of the enum member itself.

异构枚举

¥Heterogeneous enums

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

¥Technically enums can be mixed with string and numeric members, but it’s not clear why you would ever want to do so:

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

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

¥Unless you’re really trying to take advantage of JavaScript’s runtime behavior in a clever way, it’s advised that you don’t do this.

计算成员和常量成员

¥Computed and constant members

每个枚举成员都有一个与之关联的值,可以是常量或计算值。在以下情况下,枚举成员被认为是常量:

¥Each enum member has a value associated with it which can be either constant or computed. An enum member is considered constant if:

  • 它是枚举中的第一个成员,并且没有初始化器,在这种情况下,它被赋值为 0

    ¥It is the first member in the enum and it has no initializer, in which case it’s assigned the value 0:

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

    ¥It does not have an initializer and the preceding enum member was a numeric constant. In this case the value of the current enum member will be the value of the preceding enum member plus one.

    ts
    // All enum members in 'E1' and 'E2' are constant.
     
    enum E1 {
    X,
    Y,
    Z,
    }
     
    enum E2 {
    A = 1,
    B,
    C,
    }
    Try
  • 枚举成员使用常量枚举表达式进行初始化。常量枚举表达式是可以在编译时完全评估的 TypeScript 表达式的子集。一个表达式是一个常量枚举表达式,如果它是:

    ¥The enum member is initialized with a constant enum expression. A constant enum expression is a subset of TypeScript expressions that can be fully evaluated at compile time. An expression is a constant enum expression if it is:

    1. 字面枚举表达式(基本上是字符串字面或数字字面)

      ¥a literal enum expression (basically a string literal or a numeric literal)

    2. 对先前定义的常量枚举成员的引用(可以源自不同的枚举)

      ¥a reference to previously defined constant enum member (which can originate from a different enum)

    3. 带括号的常量枚举表达式

      ¥a parenthesized constant enum expression

    4. 应用于常量枚举表达式的 +-~ 一元运算符之一

      ¥one of the +, -, ~ unary operators applied to constant enum expression

    5. +-*/%<<>>>>>&|^ 以常量枚举表达式作为操作数的二元运算符

      ¥+, -, *, /, %, <<, >>, >>>, &, |, ^ binary operators with constant enum expressions as operands

    将常量枚举表达式计算为 NaNInfinity 是编译时错误。

    ¥It is a compile time error for constant enum expressions to be evaluated to NaN or Infinity.

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

¥In all other cases enum member is considered computed.

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

联合枚举和枚举成员类型

¥Union enums and enum member types

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

¥There is a special subset of constant enum members that aren’t calculated: literal enum members. A literal enum member is a constant enum member with no initialized value, or with values that are initialized to

  • 任何字符串字面(例如 "foo""bar""baz"

    ¥any string literal (e.g. "foo", "bar", "baz")

  • 任何数字字面(例如 1100

    ¥any numeric literal (e.g. 1, 100)

  • 应用于任何数字字面的一元减号(例如 -1-100

    ¥a unary minus applied to any numeric literal (e.g. -1, -100)

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

¥When all members in an enum have literal enum values, some special semantics come into play.

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

¥The first is that enum members also become types as well! For example, we can say that certain members can only have the value of an enum member:

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'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
};
Try

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

¥The other change is that enum types themselves effectively become a union of each enum member. With union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself. Because of that, TypeScript can catch bugs where we might be comparing values incorrectly. For example:

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.2367This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.
//
}
}
Try

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

¥In that example, we first checked whether x was not E.Foo. If that check succeeds, then our || will short-circuit, and the body of the ‘if’ will run. However, if the check didn’t succeed, then x can only be E.Foo, so it doesn’t make sense to see whether it’s not equal to E.Bar.

运行时的枚举

¥Enums at runtime

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

¥Enums are real objects that exist at runtime. For example, the following enum

ts
enum E {
X,
Y,
Z,
}
Try

实际上可以传递给函数

¥can actually be passed around to functions

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);
Try

编译时的枚举

¥Enums at compile time

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

¥Even though Enums are real objects that exist at runtime, the keyof keyword works differently than you might expect for typical objects. Instead, use keyof typeof to get a Type that represents all Enum keys as strings.

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");
Try

反向映射

¥Reverse mappings

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

¥In addition to creating an object with property names for members, numeric enums members also get a reverse mapping from enum values to enum names. For example, in this example:

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

TypeScript 将其编译为以下 JavaScript:

¥TypeScript compiles this down to the following JavaScript:

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

在这个生成的代码中,枚举被编译成一个对象,该对象存储正向 (name -> value) 和反向 (value -> name) 映射。对其他枚举成员的引用始终作为属性访问触发,并且从不内联。

¥In this generated code, an enum is compiled into an object that stores both forward (name -> value) and reverse (value -> name) mappings. References to other enum members are always emitted as property accesses and never inlined.

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

¥Keep in mind that string enum members do not get a reverse mapping generated at all.

const 枚举

¥const enums

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

¥In most cases, enums are a perfectly valid solution. However sometimes requirements are tighter. To avoid paying the cost of extra generated code and additional indirection when accessing enum values, it’s possible to use const enums. Const enums are defined using the const modifier on our enums:

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

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

¥Const enums can only use constant enum expressions and unlike regular enums they are completely removed during compilation. Const enum members are inlined at use sites. This is possible since const enums cannot have computed members.

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

在生成的代码中会变成

¥in generated code will become

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

常量枚举陷阱

¥Const enum pitfalls

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

¥Inlining enum values is straightforward at first, but comes with subtle implications. These pitfalls pertain to ambient const enums only (basically const enums in .d.ts files) and sharing them between projects, but if you are publishing or consuming .d.ts files, these pitfalls likely apply to you, because tsc --declaration transforms .ts files into .d.ts files.

  1. 由于 isolatedModules 文件 中列出的原因,该模式从根本上与环境常量枚举不兼容。这意味着如果你发布环境常量枚举,下引用者将无法同时使用 isolatedModules 和那些枚举值。

    ¥For the reasons laid out in the isolatedModules documentation, that mode is fundamentally incompatible with ambient const enums. This means if you publish ambient const enums, downstream consumers will not be able to use isolatedModules and those enum values at the same time.

  2. 你可以在编译时轻松地从依赖的版本 A 中内联值,并在运行时导入版本 B。版本 A 和 B 的枚举可以有不同的值,如果你不是很小心,导致 意外的缺陷,就像走错了 if 语句的分支。这些错误特别有害,因为通常在构建项目的同时运行自动化测试,具有相同的依赖版本,完全忽略了这些错误。

    ¥You can easily inline values from version A of a dependency at compile time, and import version B at runtime. Version A and B’s enums can have different values, if you are not very careful, resulting in surprising bugs, like taking the wrong branches of if statements. These bugs are especially pernicious because it is common to run automated tests at roughly the same time as projects are built, with the same dependency versions, which misses these bugs completely.

  3. importsNotUsedAsValues: "preserve" 不会忽略用作值的 const 枚举的导入,但环境 const 枚举不保证运行时 .js 文件存在。无法解析的导入会在运行时导致错误。目前明确省略导入的常用方法,仅类型导入不允许 const 枚举值

    ¥importsNotUsedAsValues: "preserve" will not elide imports for const enums used as values, but ambient const enums do not guarantee that runtime .js files exist. The unresolvable imports cause errors at runtime. The usual way to unambiguously elide imports, type-only imports, does not allow const enum values, currently.

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

¥Here are two approaches to avoiding these pitfalls:

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

    ¥Do not use const enums at all. You can easily ban const enums with the help of a linter. Obviously this avoids any issues with const enums, but prevents your project from inlining its own enums. Unlike inlining enums from other projects, inlining a project’s own enums is not problematic and has performance implications.

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

    ¥Do not publish ambient const enums, by deconstifying them with the help of preserveConstEnums. This is the approach taken internally by the TypeScript project itself. preserveConstEnums emits the same JavaScript for const enums as plain enums. You can then safely strip the const modifier from .d.ts files in a build step.

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

    ¥This way downstream consumers will not inline enums from your project, avoiding the pitfalls above, but a project can still inline its own enums, unlike banning const enums entirely.

环境枚举

¥Ambient enums

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

¥Ambient enums are used to describe the shape of already existing enum types.

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

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

¥One important difference between ambient and non-ambient enums is that, in regular enums, members that don’t have an initializer will be considered constant if its preceding enum member is considered constant. By contrast, an ambient (and non-const) enum member that does not have an initializer is always considered computed.

对象与枚举

¥Objects vs Enums

在现代 TypeScript 中,当具有 as const 的对象就足够时,你可能不需要枚举:

¥In modern TypeScript, you may not need an enum when an object with as const could suffice:

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);
Try

与 TypeScript 的 enum 相比,支持这种格式的最大参数是它使你的代码库与 JavaScript 的状态保持一致,并且 when/if 枚举被添加到 JavaScript,然后你可以转向其他语法。

¥The biggest argument in favour of this format over TypeScript’s enum is that it keeps your codebase aligned with the state of JavaScript, and when/if enums are added to JavaScript then you can move to the additional syntax.