枚举

枚举是 TypeScript 中少数几个不是 JavaScript 类型级别扩展的功能之一。

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

数字枚举

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

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

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

如果愿意,我们可以完全省略初始化程序

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

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

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

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

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

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

字符串枚举

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

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

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

异构枚举

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

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

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

计算和常量成员

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

  • 它是枚举中的第一个成员,并且没有初始化程序,在这种情况下,它被分配值为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 是编译时错误。

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

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

联合枚举和枚举成员类型

有一组特殊的常量枚举成员没有计算:文字枚举成员。文字枚举成员是没有初始化值或初始化为以下值的常量枚举成员

  • 任何字符串文字(例如 "foo""bar""baz"
  • 任何数字文字(例如 1100
  • 对任何数字文字应用一元减号(例如 -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'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
};
Try

另一个变化是枚举类型本身实际上成为每个枚举成员的联合。使用联合枚举时,类型系统能够利用它知道枚举本身中存在的确切值集这一事实。因此,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.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 没有意义。

运行时的枚举

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

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

实际上可以传递给函数

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

编译时的枚举

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

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

反向映射

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

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

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

在此生成的代码中,枚举被编译为一个对象,该对象存储正向(name -> value)和反向(value -> name)映射。对其他枚举成员的引用总是作为属性访问发出,而不会内联。

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

const 枚举

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

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

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

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

在生成的代码中将变为

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

Const 枚举陷阱

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

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

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

  1. 完全不要使用 const 枚举。你可以借助 linter 禁止 const 枚举。显然,这可以避免 const 枚举的任何问题,但会阻止你的项目内联自己的枚举。与内联其他项目的枚举不同,内联项目自己的枚举没有问题,而且会影响性能。

  2. 不要发布 ambient const 枚举,通过借助 preserveConstEnums 取消它们的不变性。这是 TypeScript 项目本身 在内部采用的方法。preserveConstEnums 为 const 枚举和普通枚举发出相同的 JavaScript。然后,你可以安全地从 .d.ts 文件中删除 const 修饰符 在构建步骤中

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

Ambient 枚举

Ambient 枚举用于描述已存在的枚举类型的形状。

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

Ambient 枚举和非 Ambient 枚举之间的一个重要区别在于,在普通枚举中,没有初始化程序的成员将被视为常量,如果其前面的枚举成员被视为常量。相比之下,没有初始化程序的 ambient(且非 const)枚举成员始终被视为计算得出的。

对象与枚举

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

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 中,那么你可以切换到额外的语法。

TypeScript 文档是一个开源项目。通过 发送拉取请求 ❤ 帮助我们改进这些页面

此页面的贡献者
OTOrta Therox (17)
FDG-SFrank de Groot - Schouten (1)
Ggreen961 (1)
TATex Andersen (1)
ETEric Telkkälä (1)
10+

上次更新:2024 年 3 月 21 日