枚举类型

枚举类型由零个或多个枚举成员构成,每个枚举成员都是一个命名的常量。在 TypeScript 中,枚举类型是一种原始类型,它通过 enum 关键字来定义。例如,我们可以使用枚举类型来表示一年四季,示例如下:

enum Season {
    Spring,
    Summer,
    Fall,
    Winter,
}

按照枚举成员的类型可以将枚举类型划分为以下三类:

  • 数值型枚举

  • 字符串枚举

  • 异构型枚举

数值型枚举

数值型枚举是最常用的枚举类型,是 number 类型的子类型,它由一组命名的数值常量构成。定义数值型枚举的方法如下所示:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

const direction: Direction = Direction.Up;

此例中,我们使用 enum 关键字定义了枚举类型 Direction,它包含了四个枚举成员 UpDownLeftRight。在使用枚举成员时,可以像访问对象属性一样访问枚举成员。

每个数值型枚举成员都表示一个具体的数字。如果在定义枚举时没有设置枚举成员的值,那么 TypeScript 将自动计算枚举成员的值。根据 TypeScript 语言的规则,第一个枚举成员的值为 0,其后每个枚举成员的值等于前一个枚举成员的值加 1。因此,Direction 枚举中 Up 的值为 0Down 的值为 1,以此类推。示例如下:

enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right,   // 3
}

在定义数值型枚举时,可以为一个或多个枚举成员设置初始值。对于未指定初始值的枚举成员,其值为前一个枚举成员的值加 1。在 5.4.5 节中将详细介绍枚举成员的计算规则。示例如下:

enum Direction {
    Up = 1,    // 1
    Down,      // 2
    Left = 10, // 10
    Right,     // 11
}

前文提到,数值型枚举是 number 类型的子类型,因此允许将数值型枚举类型赋值给 number 类型。例如,下例中常量 directionnumber 类型,可以使用数值型枚举 Direction 来初始化 direction 常量。示例如下:

enum Direction {
    Up,
    Down,
    Left,
    Right
}

const direction: number = Direction.Up;

需要注意的是,number 类型也能够赋值给枚举类型,即使 number 类型的值不在枚举成员值的列表中也不会产生错误。示例如下:

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

const d1: Direction = 0;  // Direction.Up
const d2: Direction = 10; // 不会产生错误

字符串枚举

字符串枚举与数值型枚举相似。在字符串枚举中,枚举成员的值为字符串。字符串枚举成员必须使用字符串字面量或另一个字符串枚举成员来初始化。字符串枚举成员没有自增长的行为。示例如下:

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

    U = Up,
    D = Down,
    L = Left,
    R = Right,
}

字符串枚举是 string 类型的子类型,因此允许将字符串枚举类型赋值给 string 类型。例如,下例中常量 directionstring 类型,可以使用字符串枚举 Direction 来初始化 direction 常量:

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

const direction: string = Direction.Up;

但是反过来,不允许将 string 类型赋值给字符串枚举类型,这一点与数值型枚举是不同的。例如,下例中将字符串 “'UP'” 赋值给字符串枚举类型的常量 direction 将产生编译错误:

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

const direction: Direction = 'UP';
//    ~~~~~~~~~
//    编译错误!类型 'UP' 不能赋值给类型 'Direction'

异构型枚举

TypeScript 允许在一个枚举中同时定义数值型枚举成员和字符串枚举成员,我们将这种类型的枚举称作异构型枚举。异构型枚举在实际代码中很少被使用,虽然在语法上允许定义异构型枚举,但是不推荐在代码中使用异构型枚举。我们可以尝试使用对象来代替异构型枚举。

下例中定义了一个简单的异构型枚举:

enum Color {
    Black = 0,
    White = 'White',
}

在定义异构型枚举时,不允许使用计算的值作为枚举成员的初始值。示例如下:

enum Color {
    Black = 0 + 0,
    //      ~~~~~
    //      编译错误!在带有字符串成员的枚举中不允许使用计算值

    White = 'White',
}

v5.0.4 版本之下才成立。

在异构型枚举中,必须为紧跟在字符串枚举成员之后的数值型枚举成员指定一个初始值。下例中,ColorA 枚举的定义是正确的,但是 ColorB 枚举的定义是错误的,必须为数值型枚举成员 Black 指定一个初始值。示例如下:

enum ColorA {
    Black,
    White = 'White',
}

enum ColorB {
    White = 'White',
    Black,
//  ~~~~~
//  编译错误!枚举成员必须有一个初始值
}

枚举成员映射

不论是哪种类型的枚举,都可以通过枚举成员名去访问枚举成员值。下例中,通过枚举名 Bool 和枚举成员名 FalseTrue 能够访问枚举成员的值:

enum Bool {
    False = 0,
    True = 1,
}

Bool.False;   // 0
Bool.True;    // 1

对于数值型枚举,不但可以通过枚举成员名来获取枚举成员值,也可以反过来通过枚举成员值去获取枚举成员名。下例中,通过枚举成员值 Bool.False 能够获取其对应的枚举成员名,即字符串 'False':

enum Bool {
    False = 0,
    True = 1,
}

Bool[Bool.False]; // 'False'
Bool[Bool.True];  // 'True'

对于字符串枚举和异构型枚举,则不能够通过枚举成员值去获取枚举成员名。

常量枚举成员与计算枚举成员

每个枚举成员都有一个值,根据枚举成员值的定义可以将枚举成员划分为以下两类:

  • 常量枚举成员

  • 计算枚举成员

常量枚举成员

若枚举类型的第一个枚举成员没有定义初始值,那么该枚举成员是常量枚举成员并且初始值为 0。示例如下:

enum Foo {
    A,   // 0
}

此例中,枚举成员 A 是常量枚举成员,并且 Foo.A 的值为 0。

若枚举成员没有定义初始值并且与之紧邻的前一个枚举成员值是数值型常量,那么该枚举成员是常量枚举成员并且初始值为紧邻的前一个枚举成员值加 1。如果紧邻的前一个枚举成员的值不是数值型常量,那么将产生错误。示例如下:

enum Foo {
    A,        // 0
    B,        // 1
}

enum Bar {
    C = 'C',
    D,        // 编译错误
}

此例中,枚举成员 Foo.AFoo.B 都是常量枚举成员。枚举成员 Bar.D 的定义将产生编译错误,因为它没有指定初始值并且前一个枚举成员 Bar.C 的值不是数值。

若枚举成员的初始值是常量枚举表达式,那么该枚举成员是常量枚举成员。常量枚举表达式是 TypeScript 表达式的子集,它能够在编译阶段被求值。常量枚举表达式的具体规则如下:

  • 常量枚举表达式可以是数字字面量、字符串字面量和不包含替换值的模板字面量。

  • 常量枚举表达式可以是对前面定义的常量枚举成员的引用。

  • 常量枚举表达式可以是用分组运算符包围起来的常量枚举表达式。

  • 常量枚举表达式中可以使用一元运算符 “+”、“-”、“~”,操作数必须为常量枚举表达式。

  • 常量枚举表达式中可以使用二元运算符 “+”、“-”、“*”、“**”、“/”、“%”、“<<”、“>>”、“>>>”、“&”、“|”、“^”,两个操作数必须为常量枚举表达式。

例如,下例中的枚举成员均为常量枚举成员:

enum Foo {
    A = 0,           // 数字字面量
    B = 'B',         // 字符串字面量
    C = `C`,         // 无替换值的模板字面量
    D = A,           // 引用前面定义的常量枚举成员
}

enum Bar {
    A = -1,          // 一元运算符
    B = 1 + 2,       // 二元运算符
    C = (4 / 2) * 3, // 分组运算符(小括号)
}

字面量枚举成员是常量枚举成员的子集。字面量枚举成员是指满足下列条件之一的枚举成员,具体条件如下:

  • 枚举成员没有定义初始值。

  • 枚举成员的初始值为数字字面量、字符串字面量和不包含替换值的模板字面量。

  • 枚举成员的初始值为对其他字面量枚举成员的引用。

下例中,Foo 枚举的所有成员都是字面量枚举成员,同时它们也都是常量枚举成员:

enum Foo {
    A,
    B = 1,
    C = -3,
    D = 'foo',
    E = `bar`,
    F = A
}

计算枚举成员

除常量枚举成员之外的其他枚举成员都属于计算枚举成员。下例中,枚举成员 Foo.AFoo.B 均为计算枚举成员:

enum Foo {
    A = 'A'.length,
    B = Math.pow(2, 3)
}

使用示例

枚举表示一组有限元素的集合,并通过枚举成员名来引用集合中的元素。有时候,程序中并不关注枚举成员值。在这种情况下,让 TypeScript 去自动计算枚举成员值是很方便的。示例如下:

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

function move(direction: Direction) {
    switch (direction) {
        case Direction.Up:
            console.log('Up');
            break;
        case Direction.Down:
            console.log('Down');
            break;
        case Direction.Left:
            console.log('Left');
            break;
        case Direction.Right:
            console.log('Right');
            break;
    }
}

move(Direction.Up);   // 'Up'
move(Direction.Down); // 'Down'

程序不依赖枚举成员值时,能够降低代码耦合度,使程序易于扩展。例如,我们想给 Direction 枚举添加一个名为 None 的枚举成员来表示未知方向。按照惯例,None 应作为第一个枚举成员。因此,我们可以将代码修改如下:

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

function move(direction: Direction) {
    switch (direction) {
        case Direction.None:
            console.log('None');
            break;
        case Direction.Up:
            console.log('Up');
            break;
        case Direction.Down:
            console.log('Down');
            break;
        case Direction.Left:
            console.log('Left');
            break;
        case Direction.Right:
            console.log('Right');
            break;
    }
}

move(Direction.Up);   // 'Up'
move(Direction.Down); // 'Down'
move(Direction.None); // 'None'

此例中,枚举成员 UpDownLeftRight 的值已经发生了改变,Up 的值由 0 变为 1,以此类推。由于 move() 函数的行为不直接依赖枚举成员的值,因此本次代码修改对 move() 函数的已有功能不产生任何影响。但如果程序中依赖了枚举成员的具体值,那么这次代码修改就会破坏现有的代码,如下所示:

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

function move(direction: Direction) {
    switch (direction) {
        // 不会报错,但是逻辑错误,Direction.Up的值已经不是数字0
        case 0:
            console.log('Up');
            break;

        // 省略其他代码
    }
}

联合枚举类型

当枚举类型中的所有成员都是字面量枚举成员时,该枚举类型成了联合枚举类型。

联合枚举成员类型

联合枚举类型中的枚举成员除了能够表示一个常量值外,还能够表示一种类型,即联合枚举成员类型。

下例中,Direction 枚举是联合枚举类型,Direction 枚举成员 UpDownLeftRight 既表示数值常量,也表示联合枚举成员类型:

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

const up: Direction.Up = Direction.Up;

此例最后一行,第一个 Direction.Up 表示联合枚举成员类型,第二个 Direction.Up 则表示数值常量 0。

联合枚举成员类型是联合枚举类型的子类型,因此可以将联合枚举成员类型赋值给联合枚举类型。示例如下:

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

const up: Direction.Up = Direction.Up;

const direction: Direction = up;

此例中,常量 up 的类型是联合枚举成员类型 Direction.Up,常量 direction 的类型是联合枚举类型 Direction。由于 Direction.Up 类型是 Direction 类型的子类型,因此可以将常量 up 赋值给常量 direction

联合枚举类型

联合枚举类型是由所有联合枚举成员类型构成的联合类型。示例如下:

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

type UnionDirectionType =
    | Direction.Up
    | Direction.Down
    | Direction.Left
    | Direction.Right;

此例中,Direction 枚举是联合枚举类型,它等同于联合类型 UnionDirectionType,其中 “|” 符号是定义联合类型的语法。关于联合类型的详细介绍请参考 6.3 节。

由于联合枚举类型是由固定数量的联合枚举成员类型构成的联合类型,因此编译器能够利用该性质对代码进行类型检查。示例如下:

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

function f(direction: Direction) {
    if (direction === Direction.Up) {
        // Direction.Up
    } else if (direction === Direction.Down) {
        // Direction.Down
    } else if (direction === Direction.Left) {
        // Direction.Left
    } else {
        // 能够分析出此处的direction为Direction.Right
        direction;
    }
}

此例中,编译器能够分析出 Direction 联合枚举类型只包含四种可能的联合枚举成员类型。在 “if-else” 语句中,编译器能够根据控制流分析出最后的 else 分支中 direction 的类型为 Direction.Right

下面再来看另外一个例子。Foo 联合枚举类型由两个联合枚举成员类型 Foo.AFoo.B 构成。编译器能够检查出在第 7 行 if 条件判断语句中的条件表达式结果永远为 true,因此将产生编译错误。示例如下:

enum Foo {
    A = 'A',
    B = 'B',
}

function bar(foo: Foo) {
    if (foo !== Foo.A || foo !== Foo.B) {
        //               ~~~~~~~~~~~~~
        //               编译错误:该条件永远为'true'
    }
}

让我们继续深入联合枚举类型。下例中,由于 Foo 联合枚举类型等同于联合类型 Foo.A | Foo.B,因此它是联合类型 'A' | 'B' 的子类型:

enum Foo {
    A = 'A',
    B = 'B',
}

enum Bar {
    A = 'A',
}

enum Baz {
    B = 'B',
    C = 'C',
}

function f1(x: 'A' | 'B') { // 15
    console.log(x);
}

function f2(foo: Foo, bar: Bar, baz: Baz) {
    f1(foo); // 20
    f1(bar); // 21

    f1(baz); // 23
    // ~~~
    // 错误:类型 'Baz' 不能赋值给参数类型'A' | 'B'
}

此例第 15 行,f1 函数接受 “'A' | 'B'” 联合类型的参数 x。第 20 行,允许使用 Foo 枚举类型的参数 foo 调用函数 f1,因为 Foo 枚举类型是 “'A' | 'B'” 类型的子类型。第 21 行,允许使用 Bar 枚举类型的参数 bar 调用函数 f1,因为 Bar 枚举类型是 'A' 类型的子类型,显然也是 “'A' | 'B'” 类型的子类型。第 23 行,不允许使用 Baz 枚举类型的参数 baz 调用函数 f1,因为 Baz 枚举类型是 “'B' | 'C'” 类型的子类型,显然与 “'A' | 'B'” 类型不兼容,所以会产生错误。

关于子类型兼容性的详细介绍请参考 7.1 节。

const枚举类型

枚举类型是 TypeScriptJavaScript 的扩展,JavaScript 语言本身并不支持枚举类型。在编译时,TypeScript 编译器会将枚举类型编译为 JavaScript 对象。例如,我们定义如下的枚举:

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

const d: Direction = Direction.Up;

此例中的代码编译后生成的 JavaScript 代码如下所示,为了支持枚举成员名与枚举成员值之间的正、反向映射关系,TypeScript 还生成了一些额外的代码:

"use strict";
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

const d = Direction.Up;

有时候我们不会使用枚举成员值到枚举成员名的反向映射,因此没有必要生成额外的反向映射代码,只需要生成如下代码就能够满足需求:

"use strict";
var Direction;
(function (Direction) {
    Direction["Up"] = 0;
    Direction["Down"] = 1
    Direction["Left"] = 2
    Direction["Right"] = 3
})(Direction || (Direction = {}));

const d = Direction.Up;

更进一步讲,如果我们只关注第 10 行枚举类型的使用方式就会发现,完全不需要生成与 Direction 对象相关的代码,只需要将 Direction.Up 替换为它所表示的常量 0 即可。经过此番删减后的代码量将大幅减少,并且不会改变程序的运行结果,如下所示:

"use strict";
const d = 0;

const 枚举类型具有相似的效果。const 枚举类型将在编译阶段被完全删除,并且在使用了 const 枚举类型的地方会直接将 const 枚举成员的值内联到代码中。

const 枚举类型使用 const enum 关键字定义,示例如下:

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

const directions = [
    Directions.Up,
    Directions.Down,
    Directions.Left,
    Directions.Right,
];

此例中的代码经过 TypeScript 编译器编译后生成的 JavaScript 代码如下所示:

"use strict";
const directions = [
    0 /* Up */,
    1 /* Down */,
    2 /* Left */,
    3 /* Right */
];

我们能够注意到,为了便于代码调试和保持代码的可读性,TypeScript 编译器在内联了 const 枚举成员的位置还额外添加了注释,注释的内容为枚举成员的名字。