前面在基本类型中,提到过枚举类型,前面说的都是数字枚举,而 TypeScript 支持数字的和字符串的枚举。

1. 数字枚举

首先我们看看数字枚举

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

Up 使用初始化为 1。 其余的成员会从 1 开始自动增长。 换句话说, Direction.Up 的值为 1, Down 为 2, Left 为 3, Right 为 4。

我们还可以完全不使用初始化器:

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

现在, Up 的值为 0, Down 的值为 1 等等。 当我们不在乎成员的值的时候,这种自增长的行为是很有用处的,但是要注意每个枚举成员的值都是不同的。

使用枚举很简单:通过枚举的属性来访问枚举成员,和枚举的名字来访问枚举类型

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

不带初始化器的枚举或者被放在第一的位置,或者被放在使用了数字常量或其它常量初始化了的枚举后面。 换句话说,下面的情况是不被允许的:

enum E {
   A = getSomeValue(),
   B, // 报错,因为 A 不是使用常量初始化的
}

2. 字符串枚举

字符串枚举的概念很简单,但是有细微的运行时的差别。 在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。
**

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

由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化。 换句话说,如果你正在调试并且必须要读一个数字枚举的运行时的值,这个值通常是很难读的 - 它并不能表达有用的信息(尽管反向映射会有所帮助),字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。

3. 异构枚举

从技术的角度来说,枚举可以混合字符串和数字成员,但是似乎你并不会这么做:

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

除非你真的想要利用JavaScript运行时的行为,否则我们不建议这样做。

4. 计算和常量成员

枚举成员

  1. 常量成员:会在编译时计算,直接得到结果
    • 常量
    • 未赋值
    • 常量表达式
    • 对已有枚举成员的引用
  2. 计算成员:会保留原来的表达式,在运行时计算
    • 需要计算得到的值

每个枚举成员都带有一个值,它可以是 常量计算出来的。 当满足如下条件时,枚举成员被当作是常量:

(1)它是枚举的第一个成员且没有初始化器,这种情况下它被赋予值 0:

enum E { X }
E.X //=> 0

(2)它不带有初始化器且它之前的枚举成员是一个数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值加 1。

enum E1 { X, Y, Z }
enum E2 {
    A = 1, B, C
}

(3)枚举成员使用常量枚举表达式初始化。 常数枚举表达式是 TypeScript 表达式的子集,它可以在编译阶段求值。 当一个表达式满足下面条件之一时,它就是一个常量枚举表达式

  • 一个枚举表达式字面量(主要是字符串字面量或数字字面量)
  • 一个对之前定义的常量枚举成员的引用(可以是在不同的枚举类型中定义的)
  • 带括号的常量枚举表达式
  • 一元运算符 +, -, ~ 其中之一应用在了常量枚举表达式
  • 常量枚举表达式做为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^ 的操作对象。 若常数枚举表达式求值后为 NaNInfinity,则会在编译阶段报错。
enum FileAccess {
    // 常量成员
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // 计算成员
    G = "123".length
}

5. 联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员。 字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为:

  • 任何字符串字面量(例如: “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) {
    // 报错
    // 如果第一个条件通过,那么会忽略第二个,执行 if 里面的内容
    // 如果这个检查没有通过,那么 x 只能为 E.Foo
    // 因此没有任何理由再去检查它是否为 E.Bar。
  }
}

6. 运行时的枚举

枚举是在运行时真正存在的对象

enum E {
   X, Y, Z
}
function f(obj: { X: number }) {
   return obj.X;
}
f(E); // 正确,说明 E 中确实含有 X 属性,且为 number 类型

反向映射

除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了反向映射从枚举值到枚举名字

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

TypeScript可能会将这段代码编译为下面的JavaScript:

var Enum;
(function (Enum) {
   Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。

要注意的是不会为字符串枚举成员生成反向映射

const枚举

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const 枚举。 常量枚举通过在枚举上使用 const 修饰符来定义。

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

:::info 常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。 :::

const enum Directions {
   Up,
   Down,
   Left,
   Right
}

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
// 生成后的代码为:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

7. 外部枚举

外部枚举用来描述已经存在的枚举类型的形状。枚举声明只能与命名空间或其他枚举合并。

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


外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的