枚举

枚举是 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

上面,我们有一个数字枚举,其中 Up 被初始化为 1。从这一点开始,所有以下的成员都会自动递增。换句话说,Direction.Up 的值是 1Down 的值是 2Left 的值是 3,而 Right 的值是 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

虽然字符串枚举没有自动递增的行为,但字符串枚举的好处是它们可以很好地“序列化”。 换句话说,如果你在调试时需要查看数值枚举的运行时值,这个值通常是模糊的——它本身并不能传达任何有用的信息(虽然反向映射通常可以提供帮助)。字符串枚举允许你在代码运行时提供一个有意义且可读的值,而不依赖于枚举成员本身的名称。

🌐 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

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

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

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

    对于常量枚举表达式,被计算为 NaNInfinity 是一个编译时错误。

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

🌐 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"
  • 任何数字字面量(例如 1100
  • 对任何数值字面量(例如 -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

尽管枚举(Enums)在运行时是真实存在的对象,但 keyof 关键字的工作方式与典型对象可能有所不同。相反,应使用 keyof typeof 来获取表示所有枚举键(以字符串形式)的类型。

🌐 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 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

内联枚举值起初很简单,但会带来一些微妙的影响。这些陷阱仅涉及 ambient 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和这些枚举值。
  2. 你可以在编译时轻松地将依赖 A 版本的值内联,同时在运行时导入 B 版本。A 和 B 版本的枚举可能有不同的值,如果不够小心,就可能导致令人意外的错误,比如走错 if 语句的分支。这些错误特别棘手,因为通常会在项目构建时大约同时运行自动化测试,并使用相同的依赖版本,这完全可能遗漏这些错误。
  3. importsNotUsedAsValues: "preserve" 不会对用作值的 const 枚举省略导入,但 ambient const 枚举并不能保证运行时存在 .js 文件。无法解析的导入会在运行时导致错误。通常用于明确省略导入的方式 仅类型导入 当前 不允许 const 枚举值

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

🌐 Here are two approaches to avoiding these pitfalls:

  1. 完全不要使用 const 枚举。你可以轻松地通过 linter 禁止使用 const 枚举。显然,这可以避免所有与 const 枚举相关的问题,但也会阻止你的项目对其自身的枚举进行内联。与内联其他项目的枚举不同,内联项目自身的枚举没有问题,并且对性能有影响。

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

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

环境枚举

🌐 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

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

🌐 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 的对象就足够时,可能不需要使用枚举(enum):

🌐 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 的状态保持一致,并且当/如果 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.