Skip to main content

Enum

枚举不是JavaScript的类型级拓展。它允许开发者定义一系列命名常量,更容易地记录意图或创建一组不同的类别。TypeScript中,提供了基于数字和字符串的枚举。

枚举是将一组无序但极度相关数组集合在一起来声明存储,既可以当一种类型也可以当一种存储方式,类似于对象。

枚举命名

  1. 枚举变量使用全大写、大驼峰、小驼峰。
  2. 变量之间用等号隔开。
  3. 使用等号赋值。

枚举的优点

  1. 枚举是对一组相关数组的存取。
  2. 严格规定了变量只能被赋值枚举中的数据。
  3. 很大程度上避免了程序中出现字符串或其他变量。
  4. 易于阅读和维护。

数字枚举

定义枚举需要通过enum关键字:

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

如上,定义了一个数字枚举,Up被初始化为1。从1开始,后面所有的成员都会自动递增。换句话说,Up1Down2Left3Right4

当然,也可以完全不初始化,那么将从0开始自动递增:

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

这里Up将默认为0Down1,以此类推。

有些时候,只初始化特定的某些枚举值,那么也遵守自动递增的行为:

enum Direction {
Up, // 0
Down = 2, // 2
Left = 7, // 7
Right, // 8
}

自动递增行为对于可能不关心成员值本身但关心每个值与同一枚举中的其他值不同的情况很有用。

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

enum UserResponse {
NO = 0,
YES = 1,
}
console.log(UserResponse.NO); // 0

由于数字枚举的成员会被编译为数字,因此也可以通过索引访问数字枚举的成员。如果枚举中的成员具有显式值,则它们索引将基于这些值,并且可能不是连续的整数。若不存在该数字,则返回undefiend

enum Direction {
Up = 1,
Down = 3,
Left = 5,
Right = 10,
}

console.log(Direction[1]); // Up
console.log(Direction[2]); // undefined
console.log(Direction[3]); // Down
console.log(Direction[10]); // Right

/*
编译后的结果:
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 3] = "Down";
Direction[Direction["Left"] = 5] = "Left";
Direction[Direction["Right"] = 10] = "Right";
})(Direction || (Direction = {}));
*/

数字枚举可被混合,没有初始值设定项的枚举要么需要放在首位,要么必须在使用数字常量或其他常量枚举成员初始化的数字枚举之后。

function getSomeValue(): number {
return 7;
}

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

如上代码,A动态地调用函数获得枚举值,而B也未初始化,因此无法准确的推断出B的值,会导致编译错误。解决办法为将B初始化或放在枚举首位。

多枚举声明

可以使用多个声明分离枚举,TypeScript将会自动合并它们,但是只会为各自进行值推断,因此最好显式的赋值。

enum Language { 
Chinese,
English,
}

// In an enum with multiple declarations,
// only one declaration can omit an initializer for its first enum element.
enum Language {
Japanese
}

字符串枚举

字符串枚举中,每个成员必须使用字符串字面量或着另一个字符串枚举成员初始化,如果使用同一枚举的成员初始化,那么此成员必须已经声明:

enum Direction {
// DEFAULT = UP, error
UP = 'UP',
DOWN = 'DOWN',
LEFT = 'LEFT',
RIGHT = 'RIGHT',
DEFAULT = UP,
}

虽然字符串枚举没有自动递增的行为,但是字符串枚举可以很好的”序列化”。换句话说,如果你正在调试并且必须读取数字枚举的运行时值,这些值经常是不透明的,因为它没有传达任何有用的含义。字符串枚举允许你在代码运行的时候提供有意义且可读的值,而与枚举成员本身的名称无关。

enum UserLocation {
West,
East,
South,
North,
}

enum UserLocation {
West = 'WEST',
East = 'EAST',
South = 'SOUTH',
North = 'NORTH',
}

/*
以上两种枚举方式,可以实现相同的逻辑
但是第二张看起来更具有语义化,明确的表明了其意图
*/

异构枚举

技术上来讲,字符串枚举和数字枚举可混合使用,但需要注意以下几点:

  1. 如果一个枚举成员的值是字符串,那么它后面的枚举成员必须手动指定初始值,否则会报错。
  2. 对于数字枚举成员,则采用按照自动递增行为。
  3. 在访问混合枚举成员时,需要根据成员的类型来确定返回值的类型。
enum MixinEnum {
A, // 0
B = 'a',
// error, C has no initializer after a string enum
// C,
C = 10, // 10
D, // 11
}

一般来讲,不推荐混合使用字符串和数字枚举,可能会导致以下问题:

  1. 松散类型检查。TypeScript中的枚举最终被编译成JavaScript,因此枚举值可以是任何类型。如果混合使用,则可能无法对参数进行正确的类型检查。例如,如果函数需要接受数字枚举的参数,但是实际传递了一个字符串枚举的值,而TypeScript不会抛出编译时错误。
  2. 枚举对象可能会导致混淆,它不符合通常的枚举使用方式,应尽量避免使用。
  3. 会让数字枚举失去自增特性,程序的可读性变差。
enum Color {
Red = 1,
Green = "green",
Blue = 3
}

// here, color expects a number
function printColor(color: Color) {
console.log(color);
}

printColor(Color.Red); // 1
// but get a string
printColor(Color.Green); // "green"
// it occurs error at runtime
// TypeScript 编译器无法在编译时检查传递给函数的参数值是否有效
printColor(2);

常量成员

每个枚举成员都有一个与其相关联的值,该值可以是常量,也可以是计算值。在TypeScript中,只有常量枚举才能在编译时被计算。

以下情况枚举成员会被视作常量:

  • 该成员是枚举中的第一个成员,并且没有初始化式,在这种情况下,它被赋值为0
  • 该成员没有初始化式,并且前面的枚举成员是一个数值常量。此时,当前枚举成员的值将是前一个成员的值加1,即自动递增行为。
  • 枚举成员使用常量枚举表达式初始化。常量枚举表达式是TypeScript表达式的一个子集,可以在编译时完全求值。

常量枚举表达式包含:

  • 字面量枚举表达式,即字符串或数值字面量。
  • 对先前定义的常量枚举成员的引用,这些被引用的成员可来自另一个枚举。
  • 带圆括号的常量枚举表达式。
  • 应用于常量枚举表达式的+-~一元运算符之一。
  • +-*/%<<>>>>>&|^以常量枚举表达式作为操作数的二进制操作符。
  • 将常量枚举表达式计算为NaNInfinity是一个编译时错误。
enum Parent {
ADDRESS = 'CHINA',
AGE = 50
}

enum Child {
ADDRESS = Parent.ADDRESS,
AGE = (Parent.AGE - 40) * 2,
ACCESS = 1,
SPEND = 1 << 2
}

计算成员

除了常量成员以上几种情况,其他情况都被视作计算成员。所谓计算成员,就是其值在运行是才确定,而不是编译时。

enum Test {
A = 'hello'.length
}

但使用计算成员时也有一些限制:不允许在含有字符串枚举成员的枚举中使用计算成员,只有数值枚举可以有计算成员。这主要是因为二者的有以下不同:

  1. 数字枚举支持字面量、常量枚举以及计算成员三种初始化方式,而字符串枚举只支持常量枚举初始化方式。
  2. 数字枚举在编译后会被视作对象的属性值来使用,因此可以通过计算获得;而字符串枚举在编译后会被当作对象的属性名来使用,运行时计算出来的值无法作为属性名使用。
enum Test1 {
A = 'hello',
// error. Computed values are not permitted in an enum with string valued numbers.
B = 'world'.length,
}

enum Test2 {
A = 1,
// OK
B = getB(),
}

function getB() {
return 10;
}

枚举成员类型

字面量枚举是一个常量枚举成员,没有初始值或者被初始化为数字或字符串。当所有的成员都有字面量值的时候,一些特殊的语义开始发挥作用。例如,枚举成员也可以成为类型。可以让某些成员只能具有枚举成员的值。

enum ShapeKind {
Circle,
Square,
}

interface Circle {
kind: ShapeKind.Circle;
}

interface Square {
kind: ShapeKind.Square;
}

const shape: Circle = {
// error. Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
kind: ShapeKind.Square,
};

运行时枚举

枚举是运行时真实存在的对象,并且可以作为函数的参数。

enum E {
X,
Y,
Z,
}

function f(obj: { X : number}) {
return obj.X;
}

f(E)

编译时枚举

可使用keyof typeof获取将所有枚举键表示为字符串的类型。

enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}

/**
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;

编译后,数字枚举成员还可以获得从枚举值到枚举名称的反向映射

enum Test {
A,
}
const a = Test.A; // 0
const nameOfA = Test[a]; // A

// 编译后的文件如下:
var Test;
(function (Test) {
Test[Test["A"] = 0] = "A";
})(Test || (Test = {}));
var a = Test.A; // 0
var nameOfA = Test[a]; // A

const枚举

大部分情况下,枚举是一个非常有效的解决方案。但为了避免在访问枚举值时产生额外的代码和额外的间接操作,可使用const枚举:

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

const枚举只能使用常量枚举表达式,与普通枚举不同,它们在编译过程中被完全删除。也正因此,const枚举不能有计算成员。Const enum pitfalls

const enum Test {
A,
B,
}

// after compiling
"use strict";
let directions = [
0 /* Test.A */,
1 /* Test.B */,
];

外部枚举

外部枚举用于描述已经存在的枚举类型,解决了枚举仅和自身兼容的问题。外部枚举只能在类型声明文件中定义,因为在编译成JavaScript时会擦除外部枚举定义,如果在.ts模块中定义外部枚举,运行时将找不到枚举值,从而产生运行时错误。此外,由于是在编译时生成,因此所有成员只能是常量。

定义时,需要使用declare关键字:

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

Object vs Enum

现代TypeScript中,可通过as const断言来达到和枚举相同的效果。