如何在TypeScript中使用枚举

作者选择了COVID-19救济基金,作为Write for DOnations计划的一部分接受捐赠。

简介

TypeScript中,枚举_或枚举类型是具有恒定长度的数据结构,用于保存一组常量值。这些常量值中的每一个都被称为枚举的一个_成员。枚举在设置属性或只能是一定数量的可能值时非常有用。一个常见的例子是一副扑克牌中一张牌的花色值。每张被抽出的牌要么是梅花,要么是钻石,要么是红心,要么是黑桃;除了这四种牌之外,没有任何可能的花色价值,而且这些可能的价值也不可能改变。正因为如此,一个枚举将是描述一张牌的可能花色的有效而清晰的方式。

虽然TypeScript的大多数功能在编译过程中抛出错误是有用的,但枚举作为数据结构也是有用的,它可以为你的代码保存常量。TypeScript在编译器发出的最终代码中将枚举翻译成JavaScript对象。正因为如此,你可以使用枚举来使代码库更具可读性,因为你可以将多个常量值分组在同一个数据结构中,同时也使代码的类型更加安全,而不是仅仅将不同的const 变量放在一起。

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

前提条件

要学习本教程,你将需要。

  • 一个环境,你可以在其中执行TypeScript程序,以跟上例子。要在你的本地机器上设置这个,你需要以下东西。
  • 如果你不希望在你的本地机器上创建TypeScript环境,你可以使用官方的TypeScript Playground来跟随。
  • 你需要有足够的JavaScript知识,特别是ES6+语法,如结构化、休息参数导入/导出。如果你需要关于这些主题的更多信息,建议阅读我们的《如何用JavaScript编程》系列
  • 本教程将参考支持TypeScript的文本编辑器的各个方面,并显示行内错误。这并不是使用TypeScript的必要条件,但确实能更多地利用TypeScript的特性。为了获得这些好处,你可以使用像Visual Studio Code这样的文本编辑器,它对TypeScript有开箱即用的全面支持。你也可以在TypeScript Playground中尝试这些优势。

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

在TypeScript中创建枚举

在本节中,你将通过一个声明_数字枚举_和_字符串枚举_的例子。

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

要创建一个数字枚举,请使用enum 关键字,然后是枚举的名称。然后创建一个大括号 ({}) 块,你将在里面指定枚举成员,像这样。

enum CardinalDirection {
  North = 1,
  East,
  South,
  West,
};
复制代码

在这个例子中,你正在创建一个名为CardinalDirection 的枚举,该枚举有一个成员,代表每个基本方向。枚举是保存这些选项的一个合适的数据结构选择,因为值总是只有四个选项:北、南、东和西。

你用数字1 作为你的CardinalDirection 枚举的第一个成员的值。这就把数字1 指定为North 的值。然而,你并没有为其他成员赋值。这是因为TypeScript自动将其余的成员设置为前一个成员的值加1。CardinalDirection.East 的值是2CardinalDirection.South 的值是3 ,而CardinalDirection.West 的值是4

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

你也可以完全忽略设置枚举成员的值。

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" 字符串启动了严格模式,这是一个更严格的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 返回值1 ,而CardinalDirection[1] = "North" 将值"North" 分配给键"1"

这使得数字成员的名称和它们的值之间有了双向的关系。为了测试这一点,请记录以下内容。

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代码中把枚举成员指定为类型的基本语法。这可以通过与基本类型声明相同的方式来完成。

为了在TypeScript中使用你的CardinalDirection 枚举作为变量的类型,你可以使用枚举的名称,如下面的高亮代码所示。

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;
复制代码

这是有可能的,因为1 是你的CardinalDirection 枚举的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中枚举的实际使用案例。 位标志.

位标志是一种通过使用位操作将不同的类似布尔的选项表示在一个单一变量中的方法。要做到这一点,每个标志必须精确地使用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 ,直接表示二进制数字。如果你不想使用这么大的二进制表示法,你可以使用位操作符<< (左移)。

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

1 << 0 将评估为0b00000000000000000000000000000011 << 10b00000000000000000000000000000101 << 20b00000000000000000000000000001001 << 30b0000000000000000000000000001000

现在你可以像这样声明你的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系列页面

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享