Typescript - enum 枚举类型(数值型枚举 / 字符串枚举 / 常量枚举 / 异构枚举 / 计算枚举成员 / 联合枚举和枚举成员类型 / 运行时的枚举 / 环境枚举 / 对象与枚举)教程

前言

Enums(枚举)是 TypeScript 的少数功能之一,它不是 JavaScript 的类型级扩展,仅支持数字的和基于字符串的枚举。

使用枚举您可以定义一组带名字的常量,并且清晰地表达意图或创建一组有区别的用例。

通俗介绍

其实还有很多例子,比如后端日常返回 0 / 1 等状态时,我们都可以使用枚举去定义,这样可以提高代码的可读性,便于后续的维护。

通俗来说,枚举就是一个对象的所有可能取值的集合, 在日常生活中也很常见,例如表示星期的 SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY 就可以看成是一个枚举。

您看,一个星期有 7 天(是固定的),那么你就可以把它 “枚举” 出来,供你程序使用。


再比如,后端返回的字段使用 0 - 6 标记对应的日期,这时候就可以使用枚举可提高代码可读性。

如果您还不懂的话也没关系,看了后面的示例,最后再回过头来看这里!

数值型枚举

枚举类型的值是数字类型,因此它们被称为数字类型枚举,成员的值可以不用初始化,因为具有自增行为。

首先从数字枚举开始,一个枚举可以用 enum 关键字来定义。

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

上面,我们有一个数字枚举,其中 Up 被初始化为 1 ,所有下面的成员从这一点开始自动递增。

换句话说,Direction.Up 的值是 1 ,Down 是 2,Left 是 3,Right 是 4。


如果您愿意,可以完全不使用初始化器:

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

这里,Up 的值是 0,Down 是 1,依次类推。

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


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

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

respond("Princess Caroline", UserResponse.Yes);

数字枚举可以混合在计算和常量成员中(见下文)。简而言之,没有初始化器的枚举要么需要放在第一位,要么必须放在用数字常量或其他常量枚举成员初始化的数字枚举之后。

您看,下面的情况就是不允许的:

enum E {
  A = getSomeValue(),  
  B,  
  // Ⓧ Enum成员必须有初始化器。
}

字符串枚举

在一个字符串枚举中,每个成员都必须用一个字符串字头或另一个字符串枚举成员进行常量初始化。

字符串枚举是一个类似的概念,但有一些细微的运行时差异。

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

虽然字符串枚举没有自动递增的行为,但字符串枚举有一个好处,那就是它们可以很好地 “序列化”。

换句话说,如果你在调试时不得不读取一个数字枚举的运行时值,这个值往往是不透明的,它本身并不传达任何有用的意义(尽管 反向映射 能帮你),字符串枚举允许你在代码运行时给出一个有意义的、可读的值,与枚举成员本身的名称无关。

常量枚举

常量枚举会带来一个对性能的提升。

常量枚举通过在枚举上使用 const 修饰符来定义,常量枚举不同于常规的枚举,他们会在编译阶段被删除。

const enum Size {
  WIDTH = 10,
  HEIGHT = 20
}

const area = Size.WIDTH * Size.HEIGHT; // 200

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

如上代码所示,在运行时是没有 Size 变量的,所以常量枚举会带来一个对性能的提升。

异构枚举

从技术上讲,枚举可以与字符串和数字成员混合。

枚举可以混合字符串和数字成员,但一般没必要这么做。

enum Person {
  name = 1,
  age = 2,
  love = 'LOVE',
  hobby = 'HOBBY'
}

console.log(Person.name); // 1
console.log(Person.hobby); // HOBBY

计算枚举成员

您可以通过任意表达式设置枚举成员的值。

enum NoYesNum {
  No = 123,
  Yes = Math.random(), // OK
}

这是一个数字枚举。字符串枚举和异构枚举会有更多的限制。

例如,我们不能调用某些方法来设定枚举成员的值:

enum NoYesStr {
  No = 'No',
  //@ts-ignore: Computed values are not permitted in
  // an enum with string valued members.
  Yes = ['Y', 'e', 's'].join(''),
}

联合枚举和枚举成员类型

有一个特殊的常量枚举成员的子集没有被计算:字面枚举成员。

字面枚举成员是一个没有初始化值的常量枚举成员,或者其值被初始化为:

  • 任何字符串(例如:“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, 
    // Ⓧ 类型 'ShapeKind.Square' 不能被分配给类型 'ShapeKind.Circle'
    radius: 100,
};

另一个变化是枚举类型本身有效地成为每个枚举成员的联盟。

通过联合枚举,类型系统能够利用这一事实,即它知道存在于枚举本身的精确的值集。

正因为如此,TypeScript 可以捕捉到我们可能错误地比较数值的错误。比如说:

enum E {
  Foo,
  Bar,
}
 
function f(x: E) {
  if (x !== E.Foo || x !== E.Bar) {
    // Ⓧ 这个条件将总是返回'true',因为'E.Foo'和'E.Bar'的类型没有重合。
    //...
  }
}

在这个例子中,首先检查了 x 是否是 E.Foo。

如果这个检查成功了,那么我们的 || 就会短路,if 的主体就会运行。

然而,如果检查没有成功,那么 x 就只能是 E.Foo,所以看它是否等于 E.Bar 就没有意义了。

运行时的枚举

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

例如,请看下面这个枚举:

enum E {
  X,
  Y,
  Z,
}

实际上可以被传递给函数:

enum E {
  X,
  Y,
  Z,
}
 
function f(obj: { X: number }) {
  return obj.X;
}
 
// 可以正常工作,因为'E'有一个名为'X'的属性,是一个数字。
f(E);

编译时的枚举

尽管 Enum 是在运行时存在的真实对象,keyof 关键字的工作方式与你对典型对象的预期不同。

相反,使用 keyof typeof 来获得一个将所有 Enum 键表示为字符串的类型。

enum LogLevel {
  ERROR,
  WARN,
  INFO,
  DEBUG,
} 
/** 
 * 这相当于: 
 * 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");

反向映射

除了为成员创建一个带有属性名称的对象外,数字枚举的成员还可以得到从枚举值到枚举名称的反向映射。

例如,在这个例子中:

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

TypeScript 将其编译为以下的 JavaScript:

"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) 的映射关系。

对其他枚举成员的引用总是以属性访问的方式发出,而且从不内联。

环境枚举

环境枚举是用来描述已经存在的枚举类型的形状。

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

环境枚举和非环境枚举之间的一个重要区别是,在常规枚举中,如果其前面的枚举成员被认为是常量,那么没有初始化器的成员将被认为是常量。

相反,一个没有初始化器的环境(和非常量)枚举成员总是被认为是计算的。

对象与枚举

在现代 TypeScript 中,你可能不需要一个枚举,因为一个对象的常量就足够了:

const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
 
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;
 
// (enum member) EDirection.Up = 0
EDirection.Up;
           
// (property) Up: 0
ODirection.Up;
 
// 将枚举作为一个参数
function walk(dir: EDirection) {}
 
// 它需要一个额外的行来拉出数值
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);

普通对象与 TypeScript 的枚举相比,支持这种格式的最大理由就是,

它使你的代码库与 JavaScript 的状态保持一致,when/if 枚举被添加到 JavaScript 中,那么你可以转移到额外的语法。

写在后面

关于 TS enum 枚举类型,本文只是入门教程,并非 “深度” 分析。


版权声明:本文为weixin_44198965原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。