09 枚举类型

972 阅读8分钟

08讲我们介绍了联合和交叉类型,其中有一个使用字面量联合类型来列举可能的类型(简介列举值)的场景, 比如表示星期的类型:

type Week = 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY';

const SUNDAY: Week = 'SUNDAY';
const SATURDAY: Week = 'SATURDAY';

这些有明确含义的单词来定义表示星期几的状态, 使得代码的更具有可读性。

当然, 为了更简介和高效, 我们也可以使用纯数值表示星期几。 比如 0表示'SUNDAY'

type Week = 0 | 1 | 2 | 3 | 4 | 5 | 6

那么有没有一种兼具语义化和简洁值有点的类型呢? 它就枚举(Enums), 用来表示一个被命名的整形常数的集合。

枚举类型

在TypeScript中,我们可以使用枚举定义包含被命名的常量的集合, 比如TypeScript支持数字字符两种常量值的枚举类型。

enum Week {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

上面的例子,Week既可以表示集合, 也可以表示集合的类型, 所有成员的类型都是Week的子类型。

使用tsc命令,编译上述ts代码,之后的js代码如下:

    var Week = void 0;
    (function (Week) {
        Week[Week["SUNDAY"] = 0] = "SUNDAY";
        Week[Week["MONDAY"] = 1] = "MONDAY";
        Week[Week["TUESDAY"] = 2] = "TUESDAY";
        Week[Week["WEDNESDAY"] = 3] = "WEDNESDAY";
        Week[Week["THURSDAY"] = 4] = "THURSDAY";
        Week[Week["FRIDAY"] = 5] = "FRIDAY";
        Week[Week["SATURDAY"] = 6] = "SATURDAY";
    })(Week || (Week = {}));

TypeScript转译器会把枚举类型转译为一个属性为常量,命名值从0开始递增数字映射的对象,在功能层面达到与枚举一致的效果。

7种常见的枚举类型:数字类型字符串类型异构类型常量成员和计算(值)成员枚举成员类型联合枚举常量枚举外部枚举

数字枚举

默认定义常量命名情况下,我们定义的一个默认从0开始递增的数字集合,称之为数字枚举

可以通过常量命名 = 数值来显示指定枚举成员的初始值(该数值可以指定任意类型: 整数,负数,小数等), 其他未显式指定值的成员会前一个枚举成员的数值上递增加1

enum DAY {
    SUNDAY = 1,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
}

TypeScript编译后的结果:

    var Day = void 0;
    (function (Day) {
        Day[Day["SUNDAY"] = 1] = "SUNDAY";
        Day[Day["MONDAY"] = 2] = "MONDAY";
        Day[Day["TUESDAY"] = 3] = "TUESDAY";
        Day[Day["WEDNESDAY"] = 4] = "WEDNESDAY";
        Day[Day["THURSDAY"] = 5] = "THURSDAY";
        Day[Day["FRIDAY"] = 6] = "FRIDAY";
        Day[Day["SATURDAY"] = 7] = "SATURDAY";
    })(Day || (Day = {}));

【注意1】需要避免显式指定数值,与TypeScript自动生成数值重合的情况(比如显式指定TUESDAY的数值为2,转义后MONDAY也为2,在诸如swtich场景时, 就出出现逻辑错误)。

【注意2】常量命名,结构顺序都一致的枚举,虽然转译为JavaScript后的相等的,但是在TypeScript中是不相同不满足恒等

enum test1 {
    day1
}
enum test2 {
    day1
}

test1.day1 === test2.day1; // ts2367, 始终返回false,因为test1和test2没有重叠。

字符串枚举

定义值是字符串字面量的枚举。

    enum Day {
        SUNDAY = 'SUNDAY',
        MONDAY = 'MONDAY'
    }

TypeScript转译后的结果:

    var Day = void 0;
   (function (Day) {
       Day["SUNDAY"] = "SUNDAY";
       Day["MONDAY"] = "MONDAY";
   })(Day || (Day = {}));

字符串枚举的成员在运行和调试阶段,更具备明确的含义和可读性。

异构枚举

枚举成员同时拥有数字和字符类型的成员。(鸡肋,没有使用场景

enum Day {
    SUNDAY = 'SUNDAY',
    MONDAY = 1
}

枚举成员的值可以是数字,字符串这样的常量,也可以是通过表达式计算出来的值。因此,枚举成员的可以分为常量成员计算成员

常量成员和计算(值)成员

数值为常量的枚举(字符串,数字字面量和未指定的默认情况初始值从0开始递增的数字常量)都称为常量成员。

此外,在转译时,通过被计算的常量枚举表达式定义值的成员,也被称作为常量成员, 比如以下几种情况:

  • 引用来自预先定义的常量成员,比如来自当前枚举或其他枚举
  • 圆扩弧()包裹的常量枚举表达式
  • 在常量枚举表达式上应用的一元操作符+,-,~
  • 操作常量枚举表达式的二元操作符,+ - * / % << >> >>> & | ^

除以上情况外,其他都被认为是计算值成员

    enum FileAccess {
        // 常量成员
        None,
        Read = 1 << 1,
        Write = 1 << 2,
        ReadWrite = Read | Write,
        // 计算成员
        G = '123'.length
    }

枚举成员类型和联合枚举

枚举成员和枚举类型之间的关系分两种情况: 如果枚举的成员同时包含字面量和非字面量枚举值,枚举成员的类型就是枚举本身(枚举类型本身也是本身的子类型);如果枚举成员全部是字面量枚举值,则所有枚举成员既是值又是类型,如下代码所示:

  enum Day {
    SUNDAY,
    MONDAY,
  }

  enum MyDay {
    SUNDAY,
    MONDAY = Day.MONDAY
  }

  const mondayIsDay: Day.MONDAY = Day.MONDAY; // ok: 字面量枚举成员既是值,也是类型
  const mondayIsSunday = MyDay.SUNDAY; // ok: 类型是 MyDay,MyDay.SUNDAY 仅仅是值
  const mondayIsMyDay2: MyDay.MONDAY = MyDay.MONDAY; // ts(2535),MyDay 包含非字面量值成员,所以 MyDay.MONDAY 不能作为类型

枚举仅有一个成员且是字面量成员, name这个成员的类型等于枚举类型。

enum Day {
    MONDAY
}

export const mondayIsDay : Day = Day.MONDAY; // ok
exort const mondayIsDay1: Day.MONDAY = mondayIsDay as Day; // ok

联合枚举

纯字面量成员枚举类型具有字面量类型的特性,也就等价于枚举的类型将各个成员的联合(枚举)类型。

联合类型使得TypeScript可以更清楚的枚举集合里的确切值,从而检测出一些永远不会成立的条件判断(Dead Code)。

enum Day {
    SUNDAY,
    MONDAY
}

const work = (x: Day) => {
    // 此条件将始终返回 "true",因为类型 "Day.MONDAY" 和 "Day.SUNDAY" 没有重叠。ts(2367)
    if (x !== Day.SUNDAY || x !== Day.MONDAY) {}
    
    // ok
    if(x === Day.SUNDAY || x === Day.MONDAY) {}
}

因为Day是纯字面量枚举类型,可以等价地看做联合类型 Day.SUNDAY | Day.MONDAY, 所以if恒为真,同时提示错误(错误判断逻辑见下方)

思考: 使用!==时,报错,使用===ok. 如下图

image.png

解析: 按照官方文档的解释, 先判断x !== Day.MONDAY, 如果为真, 则短路不执行||后面的判断,如果为假,此时x应该为DAY.MONDAY,然而DAY.MONDAY !== DAY.SUNDAY,所以报错。同理,使用===时,因为不确定x的具体值(||前后的判断都可能为真),所以不报错,当把||换成&&, 就会发现这个错误提示又出来了。

类型的情况:

不过,如果枚举包含需要计算(值)的成员情况就不一样了。如下示例中,TypeScript 不能区分枚举 Day 中的每个成员。因为每个成员类型都是 Day,所以无法判断出第 7 行的条件语句恒为真,也就不会提示一个 ts(2367) 错误。

  enum Day {
    SUNDAY = +'1',
    MONDAY = 'aa'.length,
  }

  const work = (x: Day) => {
    if (x !== Day.SUNDAY || x !== Day.MONDAY) { // ok
    }
  }

此外,字面量类型所具有的类型推断、类型缩小的特性,也同样适用于字面量枚举类型,如下代码所示:

  enum Day {
    SUNDAY,
    MONDAY,
  }

  let SUNDAY = Day.SUNDAY; // 类型是 Day
  const SUNDAY2 = Day.SUNDAY; // 类型 Day.SUNDAY

  const work = (x: Day) => {
    if (x === Day.SUNDAY) {
      x; // 类型缩小为 Day.SUNDAY
    }
  }

在上述代码中,我们在第 5 行通过 let 定义了一个未显式声明类型的变量 SUNDAY,TypeScript 可推断其类型是 Day;在第 6 行通过 const 定义了一个未显式声明类型的变量 SUNDAY2,TypeScript 可推断其类型是 Day.SUNDAY;在第 8 行的 if 条件判断中,变量 x 类型也从 Day 缩小为 Day.SUNDAY。

常量枚举(const enums)

枚举的作用在于定义被命名的常量集合,而 TypeScript 提供了一些途径让枚举更加易用,比如常量枚举。

我们可以通过添加 const 修饰符定义常量枚举,常量枚举定义转译为 JavaScript 之后会被移除,并在使用常量枚举成员的地方被替换为相应的内联值,因此常量枚举的成员都必须是常量成员(字面量 + 转译阶段可计算值的表达式),如下代码所示:

  const enum Day {
    SUNDAY,
    MONDAY
  }

  const work = (d: Day) => {
    switch (d) {
      case Day.SUNDAY:
        return 'take a rest';
      case Day.MONDAY:
        return 'work hard';
    }
  }
}

这里我们定义了常量枚举 Day,它的成员都是值自递增的常量成员,并且在 work 函数的 switch 分支里引用了 Day。

转译为成 JavaScript 后,Day 枚举的定义就被移除了,work 函数中对 Day 的引用也变成了常量值的引用(第 3 行内联了 0、第 5 行内联了 1),如下代码所示:

    var work = function (d) {
        switch (d) {
            case 0 /* SUNDAY */:
                return 'take a rest';
            case 1 /* MONDAY */:
                return 'work hard';
        }
    }; 

从以上示例我们可以看到,使用常量枚举不仅能减少转译后的 JavaScript 代码量(因为抹除了枚举定义),还不需要到上级作用域里查找枚举定义(因为直接内联了枚举值字面量)。

因此,通过定义常量枚举,我们可以以清晰、结构化的形式维护相关联的常量集合,比如 switch case分支,使得代码更具可读性和易维护性。而且因为转译后抹除了定义、内联成员值,所以在代码的体积和性能方面并不会比直接内联常量值差。

外部枚举(Ambient enums)

在 TypeScript 中,我们可以通过 declare 描述一个在其他地方已经定义过的变量,如下代码所示:

declare let $: any;
$('#id').addClass('show'); // ok

第 1 行我们使用 declare 描述类型是 any 的外部变量 ,在第2行则立即使用,在第 2 行则立即使用 ,此时并不会提示一个找不到 $ 变量的错误。

同样,我们也可以使用 declare 描述一个在其他地方已经定义过的枚举类型,通过这种方式定义出来的枚举类型,被称之为外部枚举,如下代码所示:

declare enum Day {
  SUNDAY,
  MONDAY,
}

const work = (x: Day) => {
  if (x === Day.SUNDAY) {
    x; // 类型是 Day
  }
}

这里我们认定在其他地方已经定义了一个 Day 这种结构的枚举,且 work 函数中使用了它。

转译为 JavaScript 之后,外部枚举的定义也会像常量枚举一样被抹除,但是对枚举成员的引用会被保留(第 2 行保留了对 Day.SUNDAY 的引用),如下代码所示:

var work = function (x) {
    if (x === Day.SUNDAY) {
        x;
    }
};

外部枚举和常规枚举的差异在于以下几点:

  • 在外部枚举中,如果没有指定初始值的成员都被当作计算(值)成员,这跟常规枚举恰好相反;
  • 即便外部枚举只包含字面量成员,这些成员的类型也不会是字面量成员类型,自然完全不具备字面量类型的各种特性。

我们可以一起使用 declare 和 const 定义外部常量枚举,使得它转译为 JavaScript 之后仍像常量枚举一样。在抹除枚举定义的同时,我们可以使用内联枚举值替换对枚举成员的引用。

外部枚举的作用在于为两个不同枚举(实际上是指向了同一个枚举类型)的成员进行兼容、比较、被复用提供了一种途径,这在一定程度上提升了枚举的可用性,让其显得不那么“鸡肋”。

参考