原文地址:Enums - Reference | TypeScript Docs

TypeScript 不完全都是 JavaScript 的类型拓展,它有一些新的特性,枚举是其中之一。

开发者可以通过枚举定义常量集合。枚举通常用来描述一组意图,或者区分不同的情况。TypeScript 提供数值枚举和字符串枚举。

数值枚举(Numeric enums)

熟悉其它语言的开发者可能对数字枚举更加熟悉。一个枚举使用 enum 关键词来定义:

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

这样,我们就定义了一个数值枚举,其中 Up 被初始化为 1,其它成员的数值自动从 1 递增。换句话说,Direction.Up 的值为 1Direction.Down 的值为 2,以此类推。也可以不为 Up 指定初始值,这样的话它就会从 0 开始,当枚举的数值本身并不重要时,这样会更方便。

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

使用枚举的方式也很简单:使用点表示法获取它的值,它的名称可以直接作为类型。

定义枚举会同时创建类型和值。

  1. enum UserResponse {
  2. No = 0,
  3. Yes = 1,
  4. }
  5. function respond(recipient: string, message: UserResponse): void {
  6. // ...
  7. }
  8. respond("Princess Caroline", UserResponse.Yes);

定义数值枚举的时候,成员的值既可以是常量,也可以是计算得到的值。但要遵循一定的规则:使用计算值的枚举成员必须在最后定义。下面这种情况会报错:

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

字符串枚举(String enums)

字符串枚举跟数字枚举类似,但它们在运行时有些区别。在字符串枚举中,每一个成员都必须赋值为常量,可以赋值为字符串字面量,也可以赋值为其它枚举成员。

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

虽然字符串枚举的值并不会自增,它依然有独特的优势。运行时的数值枚举通常是晦涩难懂的,因为数值本身难以携带更多信息,而字符串枚举的值可以携带更多有用的信息。

混合枚举(Heterogeneous enums)

从技术上讲,数值枚举和字符串枚举是可以混合使用的:

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

emmm,如果你并没有什么特殊的需求的话,并不建议采用这种方式。

计算成员和常量成员(Computed and constant members)

每个枚举成员都有一个值,可以是常量值,也可以是计算得到的值。

以下几种情况中,枚举成员会被认为拥有常量值:

  • 枚举列表的第一个成员,且没有指定值,它会被自动赋值为 0
  • 不是枚举列表的第一个成员,且没有指定值,但它前面的成员是数值常量,此时它会自动赋值为前面成员的值 +1
  • 枚举成员使用常量枚举表达式赋值,常量枚举表达式是指的可以在编译时被完全计算出结果的表达式,比如:
    • 字面量表达式,字符串字面量或者数值字面量
    • 已经定义好的枚举常量成员,可以来自其它的枚举
    • 括号括起来的常量枚举表达式
    • +-~ 等一元操作符组合的常量枚举表达式
    • 使用 +-*/%<<>>>>>&|^ 等二元操作符连接的常量枚举表达式

当常量枚举表达式计算结果为 NaN 或者 Inifity 时,编译会出错。

其它情况下,枚举成员都被视作拥有计算得到的值。

  1. enum FileAccess {
  2. // constant members
  3. None,
  4. Read = 1 << 1,
  5. Write = 1 << 2,
  6. ReadWrite = Read | Write,
  7. // computed member
  8. G = "123".length,
  9. }

联合枚举和枚举成员类型(Union enums and enum member types)

在常量枚举成员中,“字面量枚举成员”比较特殊。字面量成员包括以下情况:

  • 没有赋初始值的常量枚举成员
  • 赋值为字符串字面量的常量枚举成员
  • 赋值为数值字面量的常量枚举成员
  • 赋值为运用了 - 操作符的数字字面量的常量枚举成员

满足以上情况的枚举拥有额外的一些特性:

枚举成员也是类型

  1. enum ShapeKind {
  2. Circle,
  3. Square,
  4. }
  5. interface Circle {
  6. kind: ShapeKind.Circle;
  7. radius: number;
  8. }
  9. interface Square {
  10. kind: ShapeKind.Square;
  11. sideLength: number;
  12. }
  13. let c: Circle = {
  14. kind: ShapeKind.Square, // Error: Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
  15. radius: 100,
  16. };

枚举类型自动成为其成员的联合类型

在这个特性的基础上,TypeScript 能够高效准确地知道一个枚举类型拥有那些值,进而做更多的类型检查。

enum E {
  Foo,
  Bar,
}

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

在上面这个例子中,首先检查 x 是否等于 E.foo,如果该检查的结果是 true,其后的 || 就会被短路,if 块里的逻辑会被执行。如果该检查的结果是 false,会接着检查 x 是否等于 E.Bar,而我们知道 x 的值如果不是 Foo 的话,就只能是 Bar 了,所以这个检查是没有意义的,它最终的结果总是 true

运行时的枚举(Enums at runtime)

枚举在运行时就是对象。

enum E {
  X,
  Y,
  Z,
}

上面的这个枚举可以作为值直接传递给函数:

enum E {
  X,
  Y,
  Z,
}

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

// Works, since 'E' has a property named 'X' which is a number.
f(E);

编译时的枚举(Enums at compile time)

虽然枚举在运行时是真实的对象,但使用 keyof 操作枚举和操作一般对象的行为是不同的,如果要获得由目标枚举所有的键构成的字符串类型,请使用 keyof typeof

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");

反向映射(reverse mappings)

通过枚举创建的对象除了可以从枚举成员的名称映射到枚举成员的值之外,还包括一组反向映射,即从枚举成员的值映射到枚举成员的名称。

enum Enum {
  A,
}

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

对于上述代码,TypeScript 的编译结果为:

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

在生成的代码中,枚举被编译为一个对象,这个对象不仅保存了 name -> value 的映射,还保存了 value -> name 的映射。如果其中涉及到其它枚举,将使用对象属性访问的方式进行引用,而不是将多个对象内敛到一起。

注意:值为字符串的枚举成员根本不会生成反向映射。

const 枚举(const enums)

对于大多数情况来说,枚举是一个完美的方案。但总有一些需求是更加严格的,为了避免额外的性能损失,可以使用 const 枚举,使用 const 关键词来定义:

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

const 枚举的成员只能够使用常量枚举表达式赋值,在编译的时候,它们会被全部移除,只有值会被留在最终代码中。

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

let directions = [
  Direction.Up,
  Direction.Down,
  Direction.Left,
  Direction.Right,
];

上述代码编译结果为:

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

外部枚举(Ambient enums)

外部枚举用来为既有的枚举定义类型。

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

外部枚举和非外部枚举的区别在于:常规枚举中,如果一个未赋值成员之前的成员是常量值,则该成员也会被认为是常量值,而外部枚举中的未赋值成员将总是被视为计算值。

对象 vs 枚举(Objects vs Enums)

在 TypeScript 中,你可以使用对象 + as const 来替代枚举:

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

const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;

EDirection.Up;

(enum member) EDirection.Up = 0

ODirection.Up;

(property) Up: 0

// Using the enum as a parameter
function walk(dir: EDirection) {}

// It requires an extra line to pull out the keys
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}

walk(EDirection.Left);
run(ODirection.Right);

支持这种做法的观点认为使用对象 + as const 的方式比使用枚举更贴合 JavaScript 的语法,在 JavaScript 支持枚举(相关提案)之后,可以切换为对应的语法。