一、数字枚举

数字枚举最关键的特性是自增长属性

enum Direction {
    Up,
    Right,
    Down,
    Left
}

如果不指定 enum 中的值,则会从 0 开始,自增长为 0 / 1 / 2 / 3。

而 enum 在 typescript 中实现的原理则是通过一个立即执行函数,赋值并初始化一个对象变量。(详细见下面编译结果)

1.png

如果指定 enum 中第一个值,则会从这个 number 类型的值子增长

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

2.png

比较有意思的是,如果你从中间指定了一个值,enum 并不关心并且也不会计算这个值去生成,它总是将按照自上而下的顺序初始化:0、1、2 ...

如果遇到了初始化过值得,则停止子增长,再冲当前初始化的值开始子增长。

3.png

访问 enum 的值,很简单,直接访问即可,像对象一样。


const up = Direction.Up;

enum 声明的时候,初始化的值可以通过已发函数返回,但是一旦某个属性通过函数或者其他方式比如一个变量初始化了值,则其后面直接的属性,不能没有初始化的值,比如下面这种写法:

function getVal(): number { 
    return 1;
}

enum Direction {
    Up = getVal(),
    Right = getVal(),
    Left // 报错
}
const up = Direction.Up;

报错信息如下:

4.png

而如果后面直接属性声明了一个初始化的值,比如 0 ,并非通过函数或者变量赋值,则不会报错:

function getVal(): number { 
    return 1;
}

enum Direction {
    Up = getVal(),
    Right = getVal(),
    Down = 0,
    Left
}

二、字符串枚举

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

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

字符串枚举允许你提供一个运行时有意义的并且可读的值,独立于枚举成员的名字。

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

如果不给初始化值,则会报错:

5.jpg

三、异构枚举(Heterogeneous enums)

TypeScript 支持 Enum 的枚举成员既可以是 number 也可以是 string ,但是并不建议这样做。

enum Check {
    No = 0,
    Yes = 'YES'
}

上面代码的编译结果如下:

6.jpg

四、计算枚举成员和常量枚举成员

每个枚举成员都带有一个值,它可以是 常量计算出来(比如函数返回值或表达式) 的,当满足如下条件时,枚举成员被当作是常量:

  • 如果没有给第一个枚举成员初始化值,则初始化值是 0:
enum E {X} // 此时 E.X 是一个常量
  • 不带有初始化器且该枚举成员之前的枚举成员是一个 数字常量。 这种情况下,当前枚举成员的值为它上一个枚举成员的值 +1
enum E {
    X = 1, // E.X 也是常量
    Y, // E.Y 是常量
}
  • 枚举成员使用 常量枚举表达式 初始化。常量枚举表达式是 TypeScript 表达式的子集,可以在编译阶段就求值。当一个表达式满足下面条件之一,这个表达式就是常量枚举表达式:

    • 一个枚举表达式字面量(主要是字符字面量数字字面量
    • 一个对之前定义的常量枚举成员的引用(不限定枚举的类型)
    • 带括号的常量枚举表达式
    • 常量枚举表达式作为二元运算符 +, -, *, /, %, <<, >>, >>>, &, |, ^ 的操作对象,如果常量枚举表达式求值以后是 NaN 或者 Infinity 则会在编译阶段就报错。

所有其它情况的枚举成员被当作是需要计算得出的值。

enum FileAccess {
    // 常量枚举成员
    None,
    Read    = 1 << 1,
    Write   = 1 << 2,
    ReadWrite  = Read | Write,
    // 计算枚举成员
    G = "123".length
}

五、联合枚举与枚举成员的类型

存在一种特殊的非计算的常量枚举成员的子集:字面量枚举成员

字面量枚举成员是指不带有初始值的常量枚举成员,或者是值被初始化为以下类型

  • 任何字符串字面量(如:foo, bar, zoo
  • 任何数字字面量(如:1, 100
  • 使用了一元 - 符号的数字字面量(-1-100

当所有枚举成员都拥有字面量枚举值时,它就带有了一种特殊的语义。

首先,枚举成员成为了类型! 例如,我们可以说某些成员 只能 是枚举成员的值:

enum E { 
    X,
    Y
}
interface I_1 {
    A: E.X,
    B: E.Y
}
interface I_2 { 
    A: E.Y,
    B: E.X
}
let c: I_1 =  {
    A: E.Y, // 报错
    B: E.Y
}

上面 E enum 的 X 和 Y 都是 number,值分别是 0 和 1,然而接口 I_1 和 I_2 在声明的时候,成员属性 A 和 B 分别使用了 E.X 和 E.Y 作为他们的类型。

因此 变量 c 是 I_1 接口类型,但是 A 成员属性用了 E.Y 是有问题的:

7.jpg

另一个变化是枚举类型本身变成了每个枚举成员的 联合。 通过联合枚举,类型系统能够利用这样一个事实,它可以知道枚举里的值的集合。 因此,TypeScript能够捕获在比较值的时候犯的愚蠢的错误。 例如:

enum E {
    Foo,
    Bar
}

function f(x: E) {
    if(x !== E.Foo || x !== E.Bar) {}
}

上面代码中先检查 x 是否不是 E.Foo。 如果通过了这个检查, i f语句体里的内容会被执行。 然而,检查没有通过,那么 x 则只能为 E.Foo,因此没理由再去检查它是否为 E.Bar。

8.jpg

六、运行时的枚举

枚举是在运行时真正存在的对象。 例如下面的枚举, 可以将枚举传递给函数

enum E {
    X, Y, Z
}

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

f(E);

上面代码不会报错,因为 E 符合 obj 参数的类型约束。

反向映射

除了创建一个以属性名做为对象成员的对象之外,数字枚举成员还具有了 反向映射,从枚举值到枚举名字。 例如,在下面的例子中:

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

编译结果如下:

9.jpg

生成的代码中,枚举类型被编译成一个对象,它包含了正向映射(name -> value)和反向映射(value -> name

引用枚举成员总会生成为对属性访问并且永远也不会内联代码。

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

const 常量枚举

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。

为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,可以使用 const枚举。 常量枚举通过在枚举上使用 const 修饰符来定义。

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

常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。

常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。

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

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

可以发现,常量成员在编译之后就被删除掉了,值直接打入变量中。

10.jpg

七、declare 枚举

declare 枚举用来描述已经存在的枚举类型的形态

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

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

区别

11.jpg

12.jpg

外部枚举也是便已不存在,只有运行才知道具体的值是什么,关于外部枚举,可以看下 stackoverflow的解释: