作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。
介绍
在TypeScript 中,枚举或枚举类型是具有常量长度的数据结构,其中包含一组常量值。这些常量值中的每一个都称为枚举的成员。在设置只能是一定数量的可能值的属性或值时,枚举很有用。一个常见的例子是一副扑克牌中单张牌的花色值。每张抽到的牌都可以是梅花、钻石、红心或黑桃;除了这四个之外,没有可能的花色值,并且这些可能的值不太可能改变。因此,枚举将是一种有效且清晰的方式来描述一张牌的可能花色。
尽管 TypeScript 的大多数功能对于在编译期间抛出错误很有用,但枚举也可用作可以为代码保存常量的数据结构。TypeScript在编译器发出的最终代码中将枚举转换为JavaScript 对象。因此,您可以使用枚举使代码库更具可读性,因为您可以将多个常量值分组在同一数据结构中,同时还使代码比仅const
放置不同的变量更类型安全。
本教程将解释用于创建枚举类型的语法、TypeScript 编译器在幕后创建的 JavaScript 代码、如何提取枚举对象类型以及涉及游戏开发中位标志的枚举用例。
先决条件
要学习本教程,您需要:
- 您可以在其中执行 TypeScript 程序以跟随示例进行操作的环境。要在本地计算机上进行设置,您需要以下内容:
- 这两个节点和NPM(或丝安装才能运行的开发环境,手柄的打字稿相关的软件包)。本教程使用 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),这也适用。
- 此外,您将需要
tsc
在您的机器上安装TypeScript 编译器 ( )。为此,请参阅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 会自动将剩余成员设置为前一个成员的值加一。CardinalDirection.East
会有价值2
,CardinalDirection.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 代码中将枚举成员指定为类型的基本语法。这可以通过与声明基本类型相同的方式来完成。
要将您的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;
在这种情况下,变量只能分配给枚举的North
成员CardinalDirection
。
如果您的枚举成员具有数值,您还可以将变量的值设置为这些数值。例如,给定枚举:
enum CardinalDirection {
North = 1,
East,
South,
West,
};
您可以将类型变量的值设置CardinalDirection
为1
:
const direction: CardinalDirection = 1;
这是可能的,因为1
是枚举North
成员的值CardinalDirection
。这仅适用于枚举的数字成员,并且它依赖于已编译的 JavaScript 对数字枚举成员的双向关系,在上一节中介绍。
现在您已经尝试使用枚举值声明变量类型,下一节将演示操作枚举的特定方法:提取底层对象类型。
提取枚举的对象类型
在前面的部分中,您发现枚举不仅是 JavaScript 之上的类型级扩展,而且具有实际值。这也意味着 enum 数据结构本身有一个类型,如果您试图设置一个表示 enum 实例的 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_A
,SKILL_B
和SKILL_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
将计算为0b0000000000000000000000000000001
,1 << 1
对0b0000000000000000000000000000010
,1 << 2
对0b0000000000000000000000000000100
,和1 << 3
到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 的更多教程,请查看我们的如何在 TypeScript 中编码系列页面。