TypeScript 的 Enum 类型

Enum 是 TypeScript 新增的一种数据结构和类型,中文译为“枚举”。

简介

实际开发中,经常需要定义一组相关的常量。

  1. const RED = 1;
  2. const GREEN = 2;
  3. const BLUE = 3;
  4. let color = userInput();
  5. if (color === RED) {/* */}
  6. if (color === GREEN) {/* */}
  7. if (color === BLUE) {/* */}
  8. throw new Error('wrong color');

上面示例中,常量REDGREENBLUE是相关的,意为变量color的三个可能的取值。它们具体等于什么值其实并不重要,只要不相等就可以了。

TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。

  1. enum Color {
  2. Red, // 0
  3. Green, // 1
  4. Blue // 2
  5. }

上面示例声明了一个 Enum 结构Color,里面包含三个成员RedGreenBlue。第一个成员的值默认为整数0,第二个为1,第二个为2,以此类推。

使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。

  1. let c = Color.Green; // 1
  2. // 等同于
  3. let c = Color['Green']; // 1

Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number

  1. let c:Color = Color.Green; // 正确
  2. let c:number = Color.Green; // 正确

上面示例中,变量c的类型写成Colornumber都可以。但是,Color类型的语义更好。

Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。

  1. // 编译前
  2. enum Color {
  3. Red, // 0
  4. Green, // 1
  5. Blue // 2
  6. }
  7. // 编译后
  8. let Color = {
  9. Red: 0,
  10. Green: 1,
  11. Blue: 2
  12. };

上面示例是 Enum 结构编译前后的对比。

由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。

Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。

  1. enum Operator {
  2. ADD,
  3. DIV,
  4. MUL,
  5. SUB
  6. }
  7. function compute(
  8. op:Operator,
  9. a:number,
  10. b:number
  11. ) {
  12. switch (op) {
  13. case Operator.ADD:
  14. return a + b;
  15. case Operator.DIV:
  16. return a / b;
  17. case Operator.MUL:
  18. return a * b;
  19. case Operator.SUB:
  20. return a - b;
  21. default:
  22. throw new Error('wrong operator');
  23. }
  24. }
  25. compute(Operator.ADD, 1, 3) // 4

上面示例中,Enum 结构Operator的四个成员表示四则运算“加减乘除”。代码根本不需要用到这四个成员的值,只用成员名就够了。

Enum 作为类型有一个缺点,就是输入任何数值都不报错。

  1. enum Bool {
  2. No,
  3. Yes
  4. }
  5. function foo(noYes:Bool) {
  6. // ...
  7. }
  8. foo(33); // 不报错

上面代码中,函数foo的参数noYes只有两个可用的值,但是输入任意数值,编译都不会报错。

另外,由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)。

  1. enum Color {
  2. Red,
  3. Green,
  4. Blue
  5. }
  6. const Color = 'red'; // 报错

上面示例,Enum 结构与变量同名,导致报错。

很大程度上,Enum 结构可以被对象的as const断言替代。

  1. enum Foo {
  2. A,
  3. B,
  4. C,
  5. }
  6. const Bar = {
  7. A: 0,
  8. B: 1,
  9. C: 2,
  10. } as const;
  11. if x === Foo.A){}
  12. // 等同于
  13. if (x === Bar.A) {}

上面示例中,对象Bar使用了as const断言,作用就是使得它的属性无法修改。这样的话,FooBar的行为就很类似了,前者完全可以用后者替代,而且后者还是 JavaScript 的原生数据结构。

Enum 成员的值

Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2……

但是,也可以为 Enum 成员显式赋值。

  1. enum Color {
  2. Red,
  3. Green,
  4. Blue
  5. }
  6. // 等同于
  7. enum Color {
  8. Red = 0,
  9. Green = 1,
  10. Blue = 2
  11. }

上面示例中,Enum 每个成员的值都是显式赋值。

成员的值可以是任意数值,但不能是大整数(Bigint)。

  1. enum Color {
  2. Red = 90,
  3. Green = 0.5,
  4. Blue = 7n // 报错
  5. }

上面示例中,Enum 成员的值可以是小数,但不能是 Bigint。

成员的值甚至可以相同。

  1. enum Color {
  2. Red = 0,
  3. Green = 0,
  4. Blue = 0
  5. }

如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。

  1. enum Color {
  2. Red = 7,
  3. Green, // 8
  4. Blue // 9
  5. }
  6. // 或者
  7. enum Color {
  8. Red, // 0
  9. Green = 7,
  10. Blue // 8
  11. }

Enum 成员的值也可以使用计算式。

  1. enum Permission {
  2. UserRead = 1 << 8,
  3. UserWrite = 1 << 7,
  4. UserExecute = 1 << 6,
  5. GroupRead = 1 << 5,
  6. GroupWrite = 1 << 4,
  7. GroupExecute = 1 << 3,
  8. AllRead = 1 << 2,
  9. AllWrite = 1 << 1,
  10. AllExecute = 1 << 0,
  11. }
  12. enum Bool {
  13. No = 123,
  14. Yes = Math.random(),
  15. }

上面示例中,Enum 成员的值等于一个计算式,或者等于函数的返回值,都是正确的。

Enum 成员值都是只读的,不能重新赋值。

  1. enum Color {
  2. Red,
  3. Green,
  4. Blue
  5. }
  6. Color.Red = 4; // 报错

上面示例中,重新为 Enum 成员赋值就会报错。

为了让这一点更醒目,通常会在 enum 关键字前面加上const修饰,表示这是常量,不能再次赋值。

  1. const enum Color {
  2. Red,
  3. Green,
  4. Blue
  5. }

加上const还有一个好处,就是编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,这样能提高性能表现。

  1. const enum Color {
  2. Red,
  3. Green,
  4. Blue
  5. }
  6. const x = Color.Red;
  7. const y = Color.Green;
  8. const z = Color.Blue;
  9. // 编译后
  10. const x = 0 /* Color.Red */;
  11. const y = 1 /* Color.Green */;
  12. const z = 2 /* Color.Blue */;

上面示例中,由于 Enum 结构前面加了const关键字,所以编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。

如果希望加上const关键词后,运行时还能访问 Enum 结构(即编译后依然将 Enum 转成对象),需要在编译时打开preserveConstEnums编译选项。

同名 Enum 的合并

多个同名的 Enum 结构会自动合并。

  1. enum Foo {
  2. A,
  3. }
  4. enum Foo {
  5. B = 1,
  6. }
  7. enum Foo {
  8. C = 2,
  9. }
  10. // 等同于
  11. enum Foo {
  12. A,
  13. B = 1
  14. C = 2
  15. }

上面示例中,Foo分成三段定义,系统会自动把它们合并。

Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。

  1. enum Foo {
  2. A,
  3. }
  4. enum Foo {
  5. B, // 报错
  6. }

上面示例中,Foo的两段定义的第一个成员,都没有设置初始值,导致报错。

同名 Enum 合并时,不能有同名成员,否则报错。

  1. enum Foo {
  2. A,
  3. B
  4. }
  5. enum Foo {
  6. B = 1, // 报错
  7. C
  8. }

上面示例中,Foo的两段定义有一个同名成员B,导致报错。

同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。

  1. // 正确
  2. enum E {
  3. A,
  4. }
  5. enum E {
  6. B = 1,
  7. }
  8. // 正确
  9. const enum E {
  10. A,
  11. }
  12. const enum E {
  13. B = 1,
  14. }
  15. // 报错
  16. enum E {
  17. A,
  18. }
  19. const enum E2 {
  20. B = 1,
  21. }

同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。

字符串 Enum

Enum 成员的值除了设为数值,还可以设为字符串。也就是说,Enum 也可以用作一组相关字符串的集合。

  1. enum Direction {
  2. Up = 'UP',
  3. Down = 'DOWN',
  4. Left = 'LEFT',
  5. Right = 'RIGHT',
  6. }

上面示例中,Direction就是字符串枚举,每个成员的值都是字符串。

注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。

  1. enum Foo {
  2. A, // 0
  3. B = 'hello',
  4. C // 报错
  5. }

上面示例中,A之前没有其他成员,所以可以不设置初始值,默认等于0C之前有一个字符串成员,必须C必须有初始值,不赋值就报错了。

Enum 成员可以是字符串和数值混合赋值。

  1. enum Enum {
  2. One = 'One',
  3. Two = 'Two',
  4. Three = 3,
  5. Four = 4,
  6. }

除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。

变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。

  1. enum MyEnum {
  2. One = 'One',
  3. Two = 'Two',
  4. }
  5. let s = MyEnum.One;
  6. s = 'One'; // 报错

上面示例中,变量s的类型是MyEnum,再赋值为字符串就报错。

由于这个原因,如果函数的参数类型是字符串 Enum,传参时就不能直接传入字符串,而要传入 Enum 成员。

  1. enum MyEnum {
  2. One = 'One',
  3. Two = 'Two',
  4. }
  5. function f(arg:MyEnum) {
  6. return 'arg is ' + arg;
  7. }
  8. f('One') // 报错

上面示例中,参数类型是MyEnum,直接传入字符串会报错。

所以,字符串 Enum 作为一种类型,有限定函数参数的作用。

前面说过,数值 Enum 的成员值往往不重要。但是有些场合,开发者可能希望 Enum 成员值可以保存一些有用的信息,所以 TypeScript 才设计了字符串 Enum.

  1. const enum MediaTypes {
  2. JSON = 'application/json',
  3. XML = 'application/xml',
  4. }
  5. const url = 'localhost';
  6. fetch(url, {
  7. headers: {
  8. Accept: MediaTypes.JSON,
  9. },
  10. }).then(response => {
  11. // ...
  12. });

上面示例中,函数fetch()的参数对象的属性Accept,只能接受一些指定的字符串。这时就很适合把字符串放进一个 Enum 结构,通过成员值来引用这些字符串。

字符串 Enum 可以使用联合类型(union)代替。

  1. function move(
  2. where:'Up'|'Down'|'Left'|'Right'
  3. ) {
  4. // ...
  5. }

上面示例中,函数参数where属于联合类型,效果跟指定为字符串 Enum 是一样的。

注意,字符串 Enum 的成员值,不能使用表达式赋值。

  1. enum MyEnum {
  2. A = 'one',
  3. B = ['T', 'w', 'o'].join('') // 报错
  4. }

上面示例中,成员B的值是一个字符串表达式,导致报错。

keyof 运算符

keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。

  1. enum MyEnum {
  2. A = 'a',
  3. B = 'b'
  4. }
  5. // 'A'|'B'
  6. type Foo = keyof typeof MyEnum;

上面示例中,keyof typeof MyEnum可以取出MyEnum的所有成员名,所以类型Foo等同于联合类型'A'|'B'

注意,这里的typeof是必需的,否则keyof MyEnum相当于keyof number

  1. type Foo = keyof MyEnum;
  2. // "toString" | "toFixed" | "toExponential" |
  3. // "toPrecision" | "valueOf" | "toLocaleString"

上面示例中,类型Foo等于类型number的所有原生属性名组成的联合类型。

这是因为 Enum 作为类型,本质上属于numberstring的一种变体,而typeof MyEnum会将MyEnum当作一个值处理,从而先其转为对象类型,就可以再用keyof运算符返回该对象的所有属性名。

如果要返回 Enum 所有的成员值,可以使用in运算符。

  1. enum MyEnum {
  2. A = 'a',
  3. B = 'b'
  4. }
  5. // { a:any, b: any }
  6. type Foo = { [key in MyEnum]: any };

上面示例中,采用属性索引可以取出MyEnum的所有成员值。

反向映射

数值 Enum 存在反向映射,即可以通过成员值获得成员名。

  1. enum Weekdays {
  2. Monday = 1,
  3. Tuesday,
  4. Wednesday,
  5. Thursday,
  6. Friday,
  7. Saturday,
  8. Sunday
  9. }
  10. console.log(Weekdays[3]) // Wednesday

上面示例中,Enum 成员Wednesday的值等于3,从而可以从成员值3取到对应的成员名Wednesday,这就叫反向映射。

这是因为 TypeScript 会将上面的 Enum 结构,编译成下面的 JavaScript 代码。

  1. var Weekdays;
  2. (function (Weekdays) {
  3. Weekdays[Weekdays["Monday"] = 1] = "Monday";
  4. Weekdays[Weekdays["Tuesday"] = 2] = "Tuesday";
  5. Weekdays[Weekdays["Wednesday"] = 3] = "Wednesday";
  6. Weekdays[Weekdays["Thursday"] = 4] = "Thursday";
  7. Weekdays[Weekdays["Friday"] = 5] = "Friday";
  8. Weekdays[Weekdays["Saturday"] = 6] = "Saturday";
  9. Weekdays[Weekdays["Sunday"] = 7] = "Sunday";
  10. })(Weekdays || (Weekdays = {}));

上面代码中,实际进行了两组赋值,以第一个成员为例。

  1. Weekdays[
  2. Weekdays["Monday"] = 1
  3. ] = "Monday";

上面代码有两个赋值运算符(=),实际上等同于下面的代码。

  1. Weekdays["Monday"] = 1;
  2. Weekdays[1] = "Monday";

注意,这种情况只发生在数值 Enum,对于字符串 Enum,不存在反向映射。这是因为字符串 Enum 编译后只有一组赋值。

  1. enum MyEnum {
  2. A = 'a',
  3. B = 'b'
  4. }
  5. // 编译后
  6. var MyEnum;
  7. (function (MyEnum) {
  8. MyEnum["A"] = "a";
  9. MyEnum["B"] = "b";
  10. })(MyEnum || (MyEnum = {}));