枚举

状态: 初稿

译注: 在本章, 枚举等同于枚举组, 枚举集(集合), 代表包含若干命名常量(即枚举值)的集合; 故而, 枚举值即枚举的成员, 也称枚举成员(或成员).

介绍

定义枚举即定义一组命名常量.
运用枚举, 你能创建更清晰的程序, 例如用枚举成员代表程序中不同状态.
TypeScript 支持数值型枚举和字符串型枚举.

数值型枚举

考虑到你可能用过其他语言, 对数值型枚举比较熟悉, 我们就从数值型枚举开始.
enum 关键字定义一个枚举组.

1
2
3
4
5
6
enum Direction {
Up = 1,
Down,
Left,
Right,
}

以上, 我们定义了一个数值型枚举, 并把 Up 初始化为 1.
它(Up)其后所有枚举值将在它的基础上依次递增.
换句话说, Direction.Up = 1, .Down = 2, .Left = 3, .Right = 4.

只要你想, 不初始化任何成员也是可以的:

1
2
3
4
5
6
enum Direction {
Up,
Down,
Left,
Right,
}

枚举第一个成员 Up 自动初始化为 0, 第二个成员 Down 初始化为 1, 依此类推.
如果你在实际应用中, 只是希望获得一组能相互区分的符号, 并不关心它们的值具体是什么, 初始化的自增行为就很省事了.

枚举易于使用: 像访问对象属性一样访问枚举成员, 枚举名即枚举值的类型名:

1
2
3
4
5
6
7
8
9
10
enum Response {
No = 0,
Yes = 1,
}

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

respond("Princess Caroline", Response.Yes)

数值型枚举可以混合恒定成员与经计算成员 (见后文).
简单来说, 未初始化的成员或出现在第一位, 或出现在由数值常量或其他恒定成员初始化的成员之后.
以下定义是错误的:

1
2
3
4
enum E {
A = getSomeValue(),
B, // Error! Enum member must have initializer.
}

字符串型枚举

字符串型枚举概念相似, 如后文所述, 只在运行期存在细微差别.
字符串型枚举成员有两种初始化方式: 1. 字符串字面量; 2. 其他字符串型枚举成员.

1
2
3
4
5
6
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

字符串型枚举没有初始化自增行为, 这让它在序列化时占优.
试想, 如果你在调试程序, 运行时的枚举数值显然不会直接告诉你它代表什么 (反向映射通常能起作用), 而字符串型枚举值在运行时本身就是”可读”的, 具有独立于成员名的含义.

混合型枚举

就技术而言, 单个枚举可以混合字符串或数值型成员, 但不好解释为什么要这么做:

1
2
3
4
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}

除非你真打算聪明地从 JavaScript 运行时行为中获利, 否则, “不使用”才是明智之举.

恒定成员与经计算成员

每个枚举成员与一个值关联, 这个值可以是恒定的, 也可以是经计算的.
满足以下任意一条的, 即为恒定成员:

  • 它是枚举第一个成员, 没有手动初始化, 在这种情况下, 它有定值 0.
1
2
// E.X is constant:
enum E { X }
  • 它前一个成员与数值常量关联, 自身没有手动初始化.
    在这种情况下, 当前枚举成员的值等于前一个成员加一.
1
2
3
4
5
6
7
// 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. 以双目运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^ 连接的两个常枚举表达式

    编译器要求常枚举表达式的值不等于 NaN, 或 Infinity.

不在此类的成员皆为经计算的.

1
2
3
4
5
6
7
8
9
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)

的成员都是字面量枚举成员.

当一个枚举组里只有字面量枚举成员的时候, 特殊语义就激活了.

首先, 每个枚举成员变成单值类型!
于是, 我们能让特定类成员只能取值于某个枚举成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum ShapeKind {
Circle,
Square,
}

interface Circle {
kind: ShapeKind.Circle;
radius: number;
}

interface Square {
kind: ShapeKind.Square;
sideLength: number;
}

let c: Circle = {
kind: ShapeKind.Square, // Error! Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
}

另外一点, 枚举变成了联合其所有成员类型的自适应类型.
我们还没谈到自适应类型, 它在这里的作用是: 使类型系统获知枚举包含的确切成员.
利用这些信息, TypeScript 可以发现由无效比较引发的 bug.
例如:

1
2
3
4
5
6
7
8
9
10
11
enum E {
Foo,
Bar,
}

function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ~~~~~~~~~~~
// Error! This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
}
}

在这个例子中, 我们首先判断 x 不是 E.Foo.
如果成立, || 运算符短路, 开始执行 if 主体.
如果不成立, 表示 x 等于 E.Foo, x !== E.Bar 一定为真, 由此可见, 这条条件语句没有意义.

运行期的枚举

运行期间, 枚举是真实的对象.
例如, 下面的枚举

1
2
3
enum E {
X, Y, Z
}

可以传递给函数 f

1
2
3
4
5
6
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 一个枚举来获取它所有成员名的字符串表示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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');

反向映射

除创建一个属性服务于枚举成员的对象外, 数值型枚举还会为你创建一个值到成员名的反向映射表.
考察下例:

1
2
3
4
5
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

输出的 JavaScript 目标程序可能是:

1
2
3
4
5
6
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

我们看到, 编译生成的代码中, 枚举被转换成了一个对象, 它同时存储枚举的正向(成员名 -> )和反向( -> 成员名)映射.
对其他枚举成员的引用总是生成属性访问, 从不内联.

我们不为字符串型枚举生成反向映射表.

内联枚举

多数场景, 枚举都很不错.
但有时条件严峻得多.
为了避免生成的枚举对象和对枚举值间接访问的开销, 可以使用内联枚举.
const 修饰符来定义内联枚举:

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

和一般枚举不同, 你只能用常枚举表达式初始化它的成员, 经过编译, 枚举定义被完全移除.
编译器把枚举值内联进引用处.
这要归功于内联枚举不允许经计算成员.

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

会被编译成

1
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

外部枚举

我们用外部枚举描述已定义枚举的形体.

1
2
3
4
5
declare enum Enum {
A = 1,
B,
C = 2
}

与非外部枚举一个重要差别是, 对于后者, 我们认为恒定成员后一个未初始化成员是恒定的.
相反, 所有外部(非内联)枚举没手动初始化的成员都要经过计算.

如果这篇文章对您有用,可以考虑打赏:)