# 枚举
枚举是 TypeScript 的少数功能之一,它不是 JavaScript 的类型级扩展。
枚举允许开发人员定义一组命名常量。使用枚举可以更轻松地记录意图,或创建一组不同的案例。TypeScript 提供基于数字和基于字符串的枚举。
# 数字枚举
我们将首先从数字枚举开始,如果您来自其他语言,可能会更熟悉。可以使用 enum
关键字定义枚举。
enum Direction {
Up = 1,
Down,
Left,
Right,
}
上面,我们有一个数字枚举,其中 Up
用 1
初始化。从那时起,以下所有成员都会自动递增。换句话说,Direction.Up
的值为 1
,Down
的值为 2
,Left
的值为 3
,Right
的值为 4
。
如果我们愿意,我们可以完全不使用初始化器:
enum Direction {
Up,
Down,
Left,
Right,
}
在这里,Up
的值为 0
,Down
的值为 1
,以此类推。这种自动递增行为对于我们可能不关心成员值本身但要注意每个值与同一个中的其他值不同的情况很有用枚举。
使用枚举很简单:只需将任何成员作为枚举本身的属性访问,并使用枚举的名称声明类型:
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
respond("Princess Caroline", UserResponse.Yes);
数字枚举可以在 计算成员和常量成员(见下文)
中混合使用。简短的故事是,没有初始化器的枚举要么需要放在第一位,要么必须在使用数字常量或其他常量枚举成员初始化的数字枚举之后。换句话说,以下是不允许的:
const getSomeValue = () => 23;
enum E {
A = getSomeValue(),
B,
}
# 字符串枚举
字符串枚举是一个类似的概念,但有一些微妙的 运行时差异
,如下所述。在字符串枚举中,每个成员都必须使用字符串字面或另一个字符串枚举成员进行常量初始化。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
虽然字符串枚举没有自动递增行为,但字符串枚举的好处是它们 "serialize" 很好。换句话说,如果您正在调试并且必须读取数字枚举的运行时值,那么该值通常是不透明的——它本身并不能传达任何有用的含义(尽管 反向映射
通常可以提供帮助)。字符串枚举允许您在代码运行时提供有意义且可读的值,而与枚举成员本身的名称无关。
# 异构枚举
从技术上讲,枚举可以与字符串和数字成员混合,但不清楚你为什么要这样做:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
除非您真的想以一种巧妙的方式利用 JavaScript 的运行时行为,否则建议您不要这样做。
# 计算成员和常量成员
每个枚举成员都有一个与之关联的值,可以是常量或计算值。在以下情况下,枚举成员被认为是常量:
- 它是枚举中的第一个成员,并且没有初始化器,在这种情况下,它被赋值为
0
:
// E.X is constant:
enum E {
X,
}
- 它没有初始化程序,并且前面的枚举成员是一个数字常量。在这种情况下,当前枚举成员的值将是前一个枚举成员的值加一。
// All enum members in 'E1' and 'E2' are constant.
enum E1 {
X,
Y,
Z,
}
enum E2 {
A = 1,
B,
C,
}
枚举成员使用常量枚举表达式进行初始化。常量枚举表达式是可以在编译时完全评估的 TypeScript 表达式的子集。一个表达式是一个常量枚举表达式,如果它是:
字面枚举表达式(基本上是字符串字面或数字字面)
对先前定义的常量枚举成员的引用(可以源自不同的枚举)
带括号的常量枚举表达式
应用于常量枚举表达式的
+
、-
、~
一元运算符之一+
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
以常量枚举表达式作为操作数的二元运算符
将常量枚举表达式计算为 NaN
或 Infinity
是编译时错误。
在所有其他情况下,枚举成员被认为是计算的。
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
)
当枚举中的所有成员都具有字面枚举值时,一些特殊的语义就会发挥作用。
首先是枚举成员也变成了类型!例如,我们可以说某些成员只能具有枚举成员的值:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
radius: 100,
};
另一个变化是枚举类型本身有效地成为每个枚举成员的联合。使用联合枚举,类型系统能够利用它知道枚举本身中存在的确切值集的事实。正因为如此,TypeScript 可以捕获我们可能会错误地比较值的错误。例如:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
//
}
}
在该示例中,我们首先检查 x
是否不是 E.Foo
。如果检查成功,那么我们的 ||
将短路, 'if' 的主体将运行。但是,如果检查不成功,那么x
只能是E.Foo
,所以看它是否等于E.Bar
是没有意义的。
# 运行时的枚举
枚举是运行时存在的真实对象。例如,以下枚举
enum E {
X,
Y,
Z,
}
实际上可以传递给函数
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 键表示为字符串的类型。
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");
# 反向映射
除了为成员创建具有属性名称的对象外,数字枚举成员还获得从枚举值到枚举名称的反向映射。例如,在此示例中:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
TypeScript 将其编译为以下 JavaScript:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
在这个生成的代码中,枚举被编译成一个对象,该对象存储正向 (name
-> value
) 和反向 (value
-> name
) 映射。对其他枚举成员的引用始终作为属性访问发出,并且从不内联。
请记住,字符串枚举成员根本不会生成反向映射。
# const 枚举
在大多数情况下,枚举是一个完全有效的解决方案。然而,有时要求更严格。为了避免在访问枚举值时支付额外生成的代码和额外的间接成本,可以使用 const
枚举。常量枚举是在我们的枚举上使用 const
修饰符定义的:
const enum Enum {
A = 1,
B = A * 2,
}
常量枚举只能使用常量枚举表达式,并且与常规枚举不同,它们在编译期间会被完全删除。常量枚举成员在使用站点内联。这是可能的,因为 const 枚举不能有计算成员。
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
在生成的代码中会变成
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
# 常量枚举陷阱
内联枚举值一开始很简单,但会带来微妙的影响。这些陷阱仅与环境 const 枚举(基本上是 .d.ts
文件中的 const 枚举)以及在项目之间共享它们有关,但是如果您正在发布或使用 .d.ts
文件,这些陷阱可能适用于您,因为 tsc --declaration
将 .ts
文件转换为 .d.ts
文件。
- 由于
isolatedModules 文件
中列出的原因,该模式从根本上与环境常量枚举不兼容。这意味着如果您发布环境常量枚举,下游消费者将无法同时使用isolatedModules
和那些枚举值。 - 您可以在编译时轻松地从依赖项的版本 A 中内联值,并在运行时导入版本 B。版本 A 和 B 的枚举可以有不同的值,如果你不是很小心,导致
意外的缺陷
,就像走错了if
语句的分支。这些错误特别有害,因为通常在构建项目的同时运行自动化测试,具有相同的依赖版本,完全忽略了这些错误。 importsNotUsedAsValues: "preserve"
不会忽略用作值的 const 枚举的导入,但环境 const 枚举不保证运行时.js
文件存在。无法解析的导入会在运行时导致错误。目前明确省略导入的常用方法,仅类型导入,不允许 const 枚举值。
以下是避免这些陷阱的两种方法:
A。根本不要使用 const 枚举。在 linter 的帮助下,您可以轻松地 ban const enums
。显然,这避免了 const 枚举的任何问题,但会阻止您的项目内联自己的枚举。与其他项目的内联枚举不同,内联项目自己的枚举没有问题,并且会影响性能。B.不要发布环境常量枚举,在 preserveConstEnums
的帮助下解构它们。这是 TypeScript 项目本身
内部采用的方法。preserveConstEnums 为 const 枚举发出与普通枚举相同的 JavaScript。然后,您可以安全地从 .d.ts
文件 在构建步骤中
中删除 const
修饰符。
这样下游消费者不会从您的项目中内联枚举,避免上述陷阱,但项目仍然可以内联自己的枚举,这与完全禁止 const 枚举不同。
# 环境枚举
环境枚举用于描述已经存在的枚举类型的形状。
declare enum Enum {
A = 1,
B,
C = 2,
}
环境枚举和非环境枚举之间的一个重要区别是,在常规枚举中,如果之前的枚举成员被认为是常量,那么没有初始化器的成员将被认为是常量。相比之下,没有初始值设定项的环境(和非常量)枚举成员始终被视为已计算。
# 对象与枚举
在现代 TypeScript 中,当具有 as const
的对象就足够时,您可能不需要枚举:
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
EDirection.Up;
ODirection.Up;
// 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,然后您可以转向其他语法。