如何在TypeScript中使用枚举

来自菜鸟教程
跳转至:导航、​搜索

作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

TypeScriptenums 或枚举类型中,是具有一组恒定值的恒定长度的数据结构。 这些常量值中的每一个都称为枚举的 成员 。 在设置只能是一定数量的可能值的属性或值时,枚举很有用。 一个常见的例子是一副扑克牌中单张牌的花色值。 抽出的每张牌要么是梅花、方块、红心或黑桃; 除了这四个之外,没有可能的花色值,并且这些可能的值不太可能改变。 因此,枚举将是一种有效且清晰的方式来描述一张牌的可能花色。

虽然 TypeScript 的大多数功能都可用于在编译期间引发错误,但枚举也可用作可以保存代码常量的数据结构。 TypeScript 在编译器发出的最终代码中将枚举转换为 JavaScript 对象。 正因为如此,您可以使用枚举使代码库更具可读性,因为您可以将多个常量值分组在同一个数据结构中,同时也使代码比仅仅使用不同的 const 变量更安全大约。

本教程将解释用于创建枚举类型的语法、TypeScript 编译器在后台创建的 JavaScript 代码、如何提取枚举对象类型,以及游戏开发中涉及位标志的枚举用例。

先决条件

要遵循本教程,您将需要:

  • 一个环境,您可以在其中执行 TypeScript 程序以跟随示例。 要在本地计算机上进行设置,您将需要以下内容: 为了运行处理 TypeScript 相关包的开发环境,同时安装了 Node 和 npm(或 yarn)。 本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 进行了测试。 要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。 如果您使用的是适用于 Linux 的 Windows 子系统 (WSL),这也适用。 此外,您需要在您的机器上安装 TypeScript 编译器 (tsc)。 为此,请参阅官方 TypeScript 网站。
  • 如果您不想在本地机器上创建 TypeScript 环境,您可以使用官方的 TypeScript Playground 进行操作。
  • 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如 解构、剩余参数导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列
  • 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。 您还可以在 TypeScript Playground 中尝试这些好处。

本教程中显示的所有示例都是使用 TypeScript 4.2.3 版创建的。

在 TypeScript 中创建枚举

在本节中,您将运行一个同时声明 numeric enumstring enum 的示例。

TypeScript 中的枚举通常用于表示给定值的确定数量的选项。 这些数据排列在一组键/值对中。 虽然键必须是字符串,就像一般的 JavaScript 对象一样,枚举成员的值通常是自动递增的数字,主要用于区分一个成员和另一个成员。 只有数字值的枚举称为 numeric enums

要创建数字枚举,请使用 enum 关键字,后跟枚举的名称。 然后创建一个大括号 ({}) 块,您将在其中指定枚举成员,如下所示:

enum CardinalDirection {
  North = 1,
  East,
  South,
  West,
};

在此示例中,您正在创建一个名为 CardinalDirection 的枚举,它有一个代表每个基本方向的成员。 An enum is an appropriate choice of data structure to hold these options, since there are always only four options for values: north, south, east, and west.

您使用数字 1 作为 CardinalDirection 枚举的第一个成员的值。 这将数字 1 分配为 North 的值。 但是,您没有将值分配给其他成员。 这是因为 TypeScript 自动将剩余成员设置为前一个成员的值加一。 CardinalDirection.East 的值为 2CardinalDirection.South 的值为 3CardinalDirection.West 的值为 [X123X ]。

此行为仅适用于每个成员只有数字值的数字枚举。

您也可以完全忽略设置枚举成员的值:

enum CardinalDirection {
  North,
  East,
  South,
  West,
};

在这种情况下,TypeScript 会将第一个成员设置为 0,然后根据该成员自动设置其他成员,每个成员递增 1。 这将产生与以下相同的代码:

enum CardinalDirection {
  North = 0,
  East = 1,
  South = 2,
  West = 3,
};

TypeScript 编译器默认为枚举成员分配数字,但您可以覆盖它以生成字符串枚举。 这些是每个成员都有字符串值的枚举; 当值需要具有某种人类可读的含义时,这些很有用,例如,如果您稍后需要读取日志或错误消息中的值。

您可以使用以下代码将枚举成员声明为具有字符串值:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W'
}

现在每个方向都有一个字母值,指示它们绑定到哪个方向。

通过涵盖声明语法,您现在可以查看底层 JavaScript 以了解有关枚举行为方式的更多信息,包括键/值对的双向特性。

双向枚举成员

在 TypeScript 编译后,枚举被转换为 JavaScript 对象。 但是,枚举有一些特性可以将它们与对象区分开来。 与传统的 JavaScript 对象相比,它们为存储常量成员提供了更稳定的数据结构,并且还为枚举成员提供了双向引用。 为了展示它是如何工作的,本节将向您展示 TypeScript 如何在最终代码中编译枚举。

获取您在上一节中创建的字符串枚举:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W',
};

当使用 TypeScript 编译器编译为 JavaScript 时,这将变成以下代码:

"use strict";
var CardinalDirection;
(function (CardinalDirection) {
    CardinalDirection["North"] = "N";
    CardinalDirection["East"] = "E";
    CardinalDirection["South"] = "S";
    CardinalDirection["West"] = "W";
})(CardinalDirection || (CardinalDirection = {}));

在这段代码中,"use strict" 字符串启动了 strict mode,这是一个更严格的 JavaScript 版本。 之后,TypeScript 会创建一个没有值的变量 CardinalDirection。 然后代码包含一个 立即调用的函数表达式 (IIFE),它将 CardinalDirection 变量作为参数,同时还将其值设置为空对象 ({}) 如果它尚未设置。

在函数内部,一旦 CardinalDirection 设置为空对象,代码就会为该对象分配多个属性:

"use strict";
var CardinalDirection;
(function (CardinalDirection) {
    CardinalDirection["North"] = "N";
    CardinalDirection["East"] = "E";
    CardinalDirection["South"] = "S";
    CardinalDirection["West"] = "W";
})(CardinalDirection || (CardinalDirection = {}));

请注意,每个属性都是原始枚举的一个成员,其值设置为枚举的成员值。

对于字符串枚举,这是过程的结束。 但是接下来您将尝试使用上一节中的数字枚举进行相同的操作:

enum CardinalDirection {
  North = 1,
  East,
  South,
  West,
};

这将产生以下代码,并添加了突出显示的部分:

"use strict";
var CardinalDirection;
(function (CardinalDirection) {
    CardinalDirection[CardinalDirection["North"] = 1] = "North";
    CardinalDirection[CardinalDirection["East"] = 2] = "East";
    CardinalDirection[CardinalDirection["South"] = 3] = "South";
    CardinalDirection[CardinalDirection["West"] = 4] = "West";
})(CardinalDirection || (CardinalDirection = {}));

除了枚举的每个成员都成为对象的属性(CardinalDirection["North"] = 1])之外,枚举还为每个数字创建一个键并将字符串分配为值。 在 North 的情况下,CardinalDirection["North"] = 1 返回值 1CardinalDirection[1] = "North" 将值 "North" 分配给键 [ X122X]。

这允许在数字成员的名称和它们的值之间建立双向关系。 要对此进行测试,请记录以下内容:

console.log(CardinalDirection.North)

这将返回 "North" 键的值:

Output1

接下来,运行以下代码来反转引用的方向:

console.log(CardinalDirection[1])

输出将是:

Output"North"

为了说明代表枚举的最终对象,请将整个枚举记录到控制台:

console.log(CardinalDirection)

这将显示创建双向效果的两组键/值对:

Output{
  "1": "North",
  "2": "East",
  "3": "South",
  "4": "West",
  "North": 1,
  "East": 2,
  "South": 3,
  "West": 4
} 

了解了枚举在 TypeScript 中是如何工作的,现在您将继续使用枚举在代码中声明类型。

在 TypeScript 中使用枚举

在本节中,您将尝试在 TypeScript 代码中将枚举成员分配为类型的基本语法。 这可以通过与声明 基本类型 相同的方式来完成。

要将 CardinalDirection 枚举用作 TypeScript 中变量的类型,您可以使用枚举名称,如以下突出显示的代码所示:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W',
};

const direction: CardinalDirection = CardinalDirection.North;

请注意,您将变量设置为将枚举作为其类型:

const direction: CardinalDirection = CardinalDirection.North;

您还将变量值设置为枚举的成员之一,在本例中为 CardinalDirection.North。 您可以这样做,因为枚举被编译为 JavaScript 对象,因此它们除了作为类型之外还具有值表示。

如果您传递的值与 direction 变量的枚举类型不兼容,如下所示:

const direction: CardinalDirection = false;

TypeScript 编译器将显示错误 2322

OutputType 'false' is not assignable to type 'CardinalDirection'. (2322)

因此,direction 只能设置为 CardinalDirection 枚举的成员。

您还可以将变量的类型设置为特定的枚举成员:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W',
};

const direction: CardinalDirection.North = CardinalDirection.North;

在这种情况下,变量只能分配给 CardinalDirection 枚举的 North 成员。

如果您的枚举成员具有数值,您还可以将变量的值设置为这些数值。 例如,给定枚举:

enum CardinalDirection {
  North = 1,
  East,
  South,
  West,
};

您可以将 CardinalDirection 类型的变量的值设置为 1

const direction: CardinalDirection = 1;

这是可能的,因为 1CardinalDirection 枚举的 North 成员的值。 这仅适用于枚举的数字成员,它依赖于编译后的 JavaScript 对数字枚举成员的双向关系,在最后一节中介绍。

现在您已经尝试使用枚举值声明变量类型,下一节将演示一种操作枚举的特定方法:提取底层对象类型。

提取枚举的对象类型

在前面的部分中,您发现枚举不仅是 JavaScript 之上的类型级扩展,而且具有实际值。 这也意味着枚举数据结构本身具有类型,如果您尝试设置表示枚举实例的 JavaScript 对象,则必须考虑到该类型。 为此,您需要提取枚举对象表示本身的类型。

给定您的 CardinalDirection 枚举:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W',
};

尝试创建一个与您的枚举匹配的对象,如下所示:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W',
};

const test1: CardinalDirection = {
  North: CardinalDirection.North,
  East: CardinalDirection.East,
  South: CardinalDirection.South,
  West: CardinalDirection.West,
}

在这段代码中,test1是一个类型为CardinalDirection的对象,对象值包含了枚举的所有成员。 但是,TypeScript 编译器会显示错误 2322

OutputType '{ North: CardinalDirection; East: CardinalDirection; South: CardinalDirection; West: CardinalDirection; }' is not assignable to type 'CardinalDirection'.

这个错误的原因是 CardinalDirection 类型代表所有枚举成员的联合类型,而不是枚举对象本身的类型。 您可以通过在枚举名称前使用 typeof 来提取对象类型。 检查下面突出显示的代码:

enum CardinalDirection {
  North = 'N',
  East = 'E',
  South = 'S',
  West = 'W',
};

const test1: typeof CardinalDirection = {
  North: CardinalDirection.North,
  East: CardinalDirection.East,
  South: CardinalDirection.South,
  West: CardinalDirection.West,
}

TypeScript 编译器现在将能够正确编译您的代码。

本节展示了一种扩展枚举使用的特定方法。 接下来,您将研究一个适用于枚举的用例:游戏开发中的位标志。

使用带有 TypeScript 枚举的位标志

在本教程的最后一部分,您将了解 TypeScript 中枚举的具体用例:bit flags

位标志是一种通过使用 位运算 将不同的类似布尔值的选项表示为单个变量的方法。 为此,每个标志必须使用 32 位数字中的一位,因为这是 JavaScript 在执行按位运算时允许的最大值。 最大 32 位数是 2,147,483,647,二进制是 1111111111111111111111111111111,所以你有 31 可能的标志。

假设你正在构建一个游戏,玩家可能有不同的技能,例如 SKILL_ASKILL_BSKILL_C。 为了确保您的程序知道玩家何时具有某种技能,您可以根据玩家的状态制作可以打开或关闭的标志。

使用以下伪代码,给每个技能标志一个二进制值:

SKILL_A = 0000000000000000000000000000001
SKILL_B = 0000000000000000000000000000010
SKILL_C = 0000000000000000000000000000100

您现在可以使用按位运算符 | (OR) 将玩家的所有当前技能存储在一个变量中:

playerSkills = SKILL_A | SKILL_B

在这种情况下,使用 | 运算符为玩家分配位标志 0000000000000000000000000000001 和位标志 0000000000000000000000000000010 将产生 0000000000000000000000000000011,这将表示玩家拥有两种技能。

您还可以添加更多技能:

playerSkills |= SKILL_C

这将产生 0000000000000000000000000000111 表示玩家拥有所有三种技能。

您还可以使用按位运算符 & (AND) 和 ~ (NOT) 的组合删除技能:

playerSkills &= ~SKILL_C

然后检查玩家是否具有特定技能,使用位运算符 & (AND):

hasSkillC = (playerSkills & SKILL_C) == SKILL_C

如果玩家没有 SKILL_C 技能,(playerSkills & SKILL_C) 部分将评估为 0。 否则 (playerSkills & SKILL_C) 将计算为您正在测试的技能的确切值,在本例中为 SKILL_C (0000000000000000000000000000010)。 通过这种方式,您可以测试评估值是否与您正在测试它的技能的值相同。

由于 TypeScript 允许您将枚举成员的值设置为整数,因此您可以将这些标志存储为枚举:

enum PlayerSkills {
  SkillA = 0b0000000000000000000000000000001,
  SkillB = 0b0000000000000000000000000000010,
  SkillC = 0b0000000000000000000000000000100,
  SkillD = 0b0000000000000000000000000001000,
};

您可以使用前缀 0b 直接表示二进制数。 如果不想使用这么大的二进制表示,可以使用按位运算符 << (left shift):

enum PlayerSkills {
  SkillA = 1 << 0,
  SkillB = 1 << 1,
  SkillC = 1 << 2,
  SkillD = 1 << 3,
};

1 << 0 将计算为 0b00000000000000000000000000000011 << 10b00000000000000000000000000000101 << 20b0000000000000000000000000000100 和 [X94X ] 至 0b0000000000000000000000000001000

现在你可以像这样声明你的 playerSkills 变量:

let playerSkills: PlayerSkills = PlayerSkills.SkillA | PlayerSkills.SkillB;

注意: 必须显式设置playerSkills变量的类型为PlayerSkills,否则TypeScript会推断为number类型。


要添加更多技能,您将使用以下语法:

playerSkills |= PlayerSkills.SkillC;

您还可以删除技能:

playerSkills &= ~PlayerSkills.SkillC;

最后,您可以使用您的枚举检查玩家是否有任何给定的技能:

const hasSkillC = (playerSkills & PlayerSkills.SkillC) === PlayerSkills.SkillC;

虽然仍在底层使用位标志,但该解决方案提供了一种更具可读性和组织性的方式来显示数据。 它还通过将二进制值作为常量存储在枚举中,并在 playerSkills 变量与位标志不匹配时引发错误,从而使您的代码更加类型安全。

结论

在大多数提供类型系统的语言中,枚举是一种常见的数据结构,这在 TypeScript 中没有什么不同。 在本教程中,您在 TypeScript 中创建和使用了枚举,同时还经历了一些更高级的场景,例如提取枚举的对象类型和使用位标志。 使用枚举,您可以使代码库更具可读性,同时还将常量组织到数据结构中,而不是将它们留在全局空间中。

有关 TypeScript 的更多教程,请查看我们的 How To Code in TypeScript 系列页面