枚举是 TypeScript 中少数几个不是 JavaScript 类型级别扩展的功能之一。
枚举允许开发人员定义一组命名常量。使用枚举可以更轻松地记录意图,或创建一组不同的情况。TypeScript 提供基于数字和字符串的枚举。
数字枚举
我们首先从数字枚举开始,如果你来自其他语言,数字枚举可能更熟悉。可以使用 enum
关键字定义枚举。
tsTry
enumDirection {Up = 1,Down ,Left ,Right ,}
上面,我们有一个数字枚举,其中 Up
使用 1
初始化。从那时起,所有后续成员都自动递增。换句话说,Direction.Up
的值为 1
,Down
的值为 2
,Left
的值为 3
,Right
的值为 4
。
如果愿意,我们可以完全省略初始化程序
tsTry
enumDirection {Up ,Down ,Left ,Right ,}
在这里,Up
的值为 0
,Down
的值为 1
,依此类推。这种自动递增行为对于我们可能不在乎成员值本身,但确实在乎每个值与同一枚举中的其他值不同的情况很有用。
使用枚举很简单:只需将任何成员作为枚举本身的属性访问,并使用枚举的名称声明类型
tsTry
enumUserResponse {No = 0,Yes = 1,}functionrespond (recipient : string,message :UserResponse ): void {// ...}respond ("Princess Caroline",UserResponse .Yes );
数字枚举可以混合在 计算和常量成员中(见下文)。简而言之,没有初始化程序的枚举要么需要排在第一位,要么必须排在使用数字常量或其他常量枚举成员初始化的数字枚举之后。换句话说,以下内容是不允许的
tsTry
enumE {A =getSomeValue (),Enum member must have initializer.1061Enum member must have initializer., B }
字符串枚举
字符串枚举是一个类似的概念,但有一些微妙的 运行时差异,如下所述。在字符串枚举中,每个成员都必须使用字符串文字或另一个字符串枚举成员进行常量初始化。
tsTry
enumDirection {Up = "UP",Down = "DOWN",Left = "LEFT",Right = "RIGHT",}
虽然字符串枚举没有自动递增行为,但字符串枚举具有“序列化”良好的优点。换句话说,如果你正在调试并且必须读取数字枚举的运行时值,则该值通常是不透明的——它本身不会传达任何有用的含义(尽管反向映射通常可以提供帮助)。字符串枚举允许你在代码运行时提供一个有意义且可读的值,而与枚举成员本身的名称无关。
异构枚举
从技术上讲,枚举可以与字符串和数字成员混合使用,但目前尚不清楚你为什么要这样做
tsTry
enumBooleanLikeHeterogeneousEnum {No = 0,Yes = "YES",}
除非你真的想以一种聪明的方式利用 JavaScript 的运行时行为,否则建议你不要这样做。
计算和常量成员
每个枚举成员都有一个与其关联的值,该值可以是常量或计算。如果枚举成员满足以下条件,则它被视为常量
-
它是枚举中的第一个成员,并且没有初始化程序,在这种情况下,它被分配值为
0
ts
Try// E.X is constant:enumE {X ,} -
它没有初始化程序,并且前面的枚举成员是数字常量。在这种情况下,当前枚举成员的值将是前面枚举成员的值加一。
ts
Try// All enum members in 'E1' and 'E2' are constant.enumE1 {X ,Y ,Z ,}enumE2 {A = 1,B ,C ,} -
枚举成员使用常量枚举表达式进行初始化。常量枚举表达式是 TypeScript 表达式的子集,可以在编译时完全求值。如果表达式是
- 文字枚举表达式(基本上是字符串文字或数字文字)
- 对先前定义的常量枚举成员的引用(可以来自不同的枚举)
- 带括号的常量枚举表达式
- 对常量枚举表达式应用的
+
、-
、~
一元运算符之一 - 使用常量枚举表达式作为操作数的
+
、-
、*
、/
、%
、<<
、>>
、>>>
、&
、|
、^
二元运算符
常量枚举表达式求值为
NaN
或Infinity
是编译时错误。
在所有其他情况下,枚举成员都被视为已计算。
tsTry
enumFileAccess {// constant membersNone ,Read = 1 << 1,Write = 1 << 2,ReadWrite =Read |Write ,// computed memberG = "123".length ,}
联合枚举和枚举成员类型
有一组特殊的常量枚举成员没有计算:文字枚举成员。文字枚举成员是没有初始化值或初始化为以下值的常量枚举成员
- 任何字符串文字(例如
"foo"
、"bar"
、"baz"
) - 任何数字文字(例如
1
、100
) - 对任何数字文字应用一元减号(例如
-1
、-100
)
当枚举中的所有成员具有枚举字面值时,一些特殊语义就会发挥作用。
首先,枚举成员也会成为类型!例如,我们可以说某些成员只能具有枚举成员的值
tsTry
enumShapeKind {Circle ,Square ,}interfaceCircle {kind :ShapeKind .Circle ;radius : number;}interfaceSquare {kind :ShapeKind .Square ;sideLength : number;}letc :Circle = {Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.2322Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.: kind ShapeKind .Square ,radius : 100,};
另一个变化是枚举类型本身实际上成为每个枚举成员的联合。使用联合枚举时,类型系统能够利用它知道枚举本身中存在的确切值集这一事实。因此,TypeScript 可以捕获我们可能不正确比较值时的错误。例如
tsTry
enumE {Foo ,Bar ,}functionf (x :E ) {if (This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.2367This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.x !==E .Foo ||x !==E .Bar ) {//}}
在该示例中,我们首先检查了 x
是否不为 E.Foo
。如果该检查成功,则我们的 ||
将短路,并且“if”的主体将运行。但是,如果检查未成功,则 x
只能为 E.Foo
,因此查看它是否不等于 E.Bar
没有意义。
运行时的枚举
枚举是运行时存在的真实对象。例如,以下枚举
tsTry
enumE {X ,Y ,Z ,}
实际上可以传递给函数
tsTry
enumE {X ,Y ,Z ,}functionf (obj : {X : number }) {returnobj .X ;}// Works, since 'E' has a property named 'X' which is a number.f (E );
编译时的枚举
即使枚举是运行时存在的真实对象,keyof
关键字的工作方式与您对典型对象的预期不同。相反,使用 keyof typeof
来获取一个类型,该类型将所有枚举键表示为字符串。
tsTry
enumLogLevel {ERROR ,WARN ,INFO ,DEBUG ,}/*** This is equivalent to:* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';*/typeLogLevelStrings = keyof typeofLogLevel ;functionprintImportant (key :LogLevelStrings ,message : string) {constnum =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");
反向映射
除了为成员创建带有属性名称的对象之外,数字枚举成员还从枚举值获取到枚举名称的反向映射。例如,在此示例中
tsTry
enumEnum {A ,}leta =Enum .A ;letnameOfA =Enum [a ]; // "A"
TypeScript 编译为以下 JavaScript
tsTry
"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
枚举。使用枚举上的 const
修饰符来定义 const
枚举
tsTry
const enumEnum {A = 1,B =A * 2,}
const
枚举只能使用常量枚举表达式,并且与常规枚举不同,它们在编译期间被完全移除。const
枚举成员在使用站点内联。这是可能的,因为 const
枚举不能具有计算成员。
tsTry
const enumDirection {Up ,Down ,Left ,Right ,}letdirections = [Direction .Up ,Direction .Down ,Direction .Left ,Direction .Right ,];
在生成的代码中将变为
tsTry
"use strict";let directions = [0 /* Direction.Up */,1 /* Direction.Down */,2 /* Direction.Left */,3 /* Direction.Right */,];
Const 枚举陷阱
内联枚举值起初很简单,但会带来一些微妙的影响。这些陷阱仅与环境 const 枚举(基本上是 .d.ts
文件中的 const 枚举)以及在项目之间共享它们有关,但是如果你正在发布或使用 .d.ts
文件,则这些陷阱很可能适用于你,因为 tsc --declaration
会将 .ts
文件转换为 .d.ts
文件。
- 由于
isolatedModules
文档 中列出的原因,该模式与环境 const 枚举在根本上不兼容。这意味着如果你发布环境 const 枚举,下游使用者将无法同时使用isolatedModules
和那些枚举值。 - 你可以轻松地在编译时内联依赖项的 A 版本的值,并在运行时导入 B 版本。如果你不够小心,版本 A 和 B 的枚举可能具有不同的值,从而导致 令人惊讶的错误,例如采用
if
语句的错误分支。这些错误特别有害,因为通常在与项目构建大致相同的时间使用相同依赖项版本运行自动化测试,这会完全错过这些错误。 importsNotUsedAsValues: "preserve"
不会省略用作值的 const 枚举的导入,但环境 const 枚举不能保证存在运行时的.js
文件。无法解析的导入会导致运行时错误。明确省略导入的通常方法,仅类型导入,当前不允许使用 const 枚举值。
以下是避免这些陷阱的两种方法
-
完全不要使用 const 枚举。你可以借助 linter 禁止 const 枚举。显然,这可以避免 const 枚举的任何问题,但会阻止你的项目内联自己的枚举。与内联其他项目的枚举不同,内联项目自己的枚举没有问题,而且会影响性能。
-
不要发布 ambient const 枚举,通过借助
preserveConstEnums
取消它们的不变性。这是 TypeScript 项目本身 在内部采用的方法。preserveConstEnums
为 const 枚举和普通枚举发出相同的 JavaScript。然后,你可以安全地从.d.ts
文件中删除const
修饰符 在构建步骤中。这样,下游使用者将不会内联你的项目中的枚举,避免上述缺陷,但项目仍然可以内联自己的枚举,这与完全禁止 const 枚举不同。
Ambient 枚举
Ambient 枚举用于描述已存在的枚举类型的形状。
tsTry
declare enumEnum {A = 1,B ,C = 2,}
Ambient 枚举和非 Ambient 枚举之间的一个重要区别在于,在普通枚举中,没有初始化程序的成员将被视为常量,如果其前面的枚举成员被视为常量。相比之下,没有初始化程序的 ambient(且非 const)枚举成员始终被视为计算得出的。
对象与枚举
在现代 TypeScript 中,当一个带有 as const
的对象足够时,你可能不需要枚举
tsTry
const enumEDirection {Up ,Down ,Left ,Right ,}constODirection = {Up : 0,Down : 1,Left : 2,Right : 3,} asconst ;EDirection .Up ;ODirection .Up ;// Using the enum as a parameterfunctionwalk (dir :EDirection ) {}// It requires an extra line to pull out the valuestypeDirection = typeofODirection [keyof typeofODirection ];functionrun (dir :Direction ) {}walk (EDirection .Left );run (ODirection .Right );
与 TypeScript 的 enum
相比,这种格式最大的优势在于它使你的代码库与 JavaScript 的状态保持一致,并且 当/如果 枚举被添加到 JavaScript 中,那么你可以切换到额外的语法。