TypeScript枚举用法解读
在 JavaScript 原生语言中并没有与枚举匹配的概念,而 TypeScript 中实现了枚举类型(Enums),这就意味着枚举也是 TypeScript 特有的语法
# 枚举类型
在 TypeScript 中,我们可以使用枚举定义包含被命名的常量的集合,比如 TypeScript 支持数字、字符两种常量值的枚举类型。
我们也可以使用 enum 关键字定义枚举类型,格式是 enum + 枚举名字 + 一对花括弧,花括弧里则是被命名了的常量成员。
下面我们把前边表示星期的联合类型示例使用枚举类型实现一遍,如下代码所示
enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
2
3
4
5
6
7
8
9
在上述示例中,Day 既可以表示集合,也可以表示集合的类型,所有成员(enum member)的类型都是 Day 的子类型。
前边我们说过,JavaScript 中其实并没有与枚举类型对应的原始实现,而 TypeScript 转译器会把枚举类型转译为一个属性为常量、命名值从 0 开始递增数字映射的对象,在功能层面达到与枚举一致的效果(然而不是所有的特性在 JavaScript 中都有对应的实现)。
下面我们通过如下所示示例看看将如上示例转译为 JavaScript 后的效果。
var Day = void 0;
(function (Day) {
Day[Day["SUNDAY"] = 0] = "SUNDAY";
Day[Day["MONDAY"] = 1] = "MONDAY";
Day[Day["TUESDAY"] = 2] = "TUESDAY";
Day[Day["WEDNESDAY"] = 3] = "WEDNESDAY";
Day[Day["THURSDAY"] = 4] = "THURSDAY";
Day[Day["FRIDAY"] = 5] = "FRIDAY";
Day[Day["SATURDAY"] = 6] = "SATURDAY";
})(Day || (Day = {}));
2
3
4
5
6
7
8
9
10
我们可以看到 Day.SUNDAY 被赋予 0 作为值,Day.SATURDAY 被赋予 6 作为值。
在 TypeScript 中,我们可以通过“枚举名字.常量命名”的格式获取枚举集合里的成员,如下代码所示:
function work(d: Day) {
switch (d) {
case Day.SUNDAY:
case Day.SATURDAY:
return 'take a rest';
case Day.MONDAY:
case Day.TUESDAY:
case Day.WEDNESDAY:
case Day.THURSDAY:
case Day.FRIDAY:
return 'work hard';
}
}
// 函数转译为 JavaScript 后,里面的 switch 分支运行时的效果实际上等价于如下所示代码:
switch (d) {
case 0:
case 1:
return 'take a rest';
case 2:
case 3:
case 4:
case 5:
case 6:
return 'work hard';
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这就意味着在 JavaScript 中调用 work 函数时,传递的参数无论是 enum 还是数值,逻辑上将没有区别,当然这也符合 TypeScript 静态类型检测规则,如下代码所示:
work(Day.SUNDAY); // ok
work(0); // ok
2
这里我们既可以把枚举成员 Day.SUNDAY 作为 work 函数的入参,也可以把数字字面量 0 作为 work 函数的入参。
# 数字枚举
默认从 0 开始递增的数字集合,称之为数字枚举。
如果我们希望枚举值从其他值开始递增,则可以通过“常量命名 = 数值” 的格式显示指定枚举成员的初始值,如下代码所示:
enum Day {
SUNDAY = 1,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
2
3
4
5
6
7
8
9
我们指定了从 1 开始递增。而后面的枚举成员若是不指定初始值,则从1开始递增,直到SATURDAY = 7 结束。 当然我们也可以给任意位置的成员指定值,如下所示示例:
enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY = 5
}
2
3
4
5
6
7
8
9
当然这个时候的结果就比较尴尬了,ts会默认SUNDAY从0开始递增,这时候就会出现 Day["FRIDAY"] = 5 和 Day["SATURDAY"] = 5 两个分支,从而发生逻辑错误。所以说,由于枚举默认的值自递增且完全无法保证稳定性,所以给部分数字类型的枚举成员显式指定数值或给函数传递数值而不是枚举类型作为入参都属于不明智的行为
此外,常量命名、结构顺序都一致的两个枚举,即便转译为 JavaScript 后,同名成员的值仍然一样(满足恒等 === )。但在 TypeScript 看来,它们不相同、不满足恒等,如下代码所示:
enum MyDay {
SUNDAY,
...
}
Day.SUNDAY === MyDay.SUNDAY; // ts(2367) 两个枚举值恒不相等
work(MyDay.SUNDAY); // ts(2345) 'MyDay.SUNDAY' 不能赋予 'Day'
2
3
4
5
6
7
不仅仅是数字类型枚举,所有其他枚举都仅和自身兼容,这就消除了由于枚举不稳定性可能造成的风险,所以这是一种极其安全的设计。不过,这可能会使得枚举变得不那么好用,因为不同枚举之间完全不兼容,所以不少 TypeScript 编程人员觉得枚举类型是一种十分鸡肋的类型。而两个结构完全一样的枚举类型如果互相兼容,则会更符合我们的预期,比如说基于 Swagger 自动生成的不同模块中结构相同且描述同一个常量集合的多个同名枚举。
# 字符串枚举
我们将枚举的常量值定义为字符串字面量的枚举成为字符串枚举
enum Day {
SUNDAY = 'SUNDAY',
MONDAY = 'MONDAY',
...
}
// 转义后
var Day = void 0;
(function (Day) {
Day["SUNDAY"] = "SUNDAY";
Day["MONDAY"] = "MONDAY";
})(Day || (Day = {}));
2
3
4
5
6
7
8
9
10
11
12
# 异构枚举
从技术上来讲,TypeScript 支持枚举类型同时拥有数字和字符类型的成员,这样的枚举被称之为异构枚举。
enum Day {
SUNDAY = 'SUNDAY',
MONDAY = 2,
...
}
2
3
4
5
枚举成员的值既可以是数字、字符串这样的常量,也可以是通过表达式所计算出来的值。这就涉及枚举里成员的一个分类,即常量成员和计算成员。
# 常量成员和计算(值)成员
涉及的枚举成员的值都是字符串、数字字面量和未指定初始值从 0 递增数字常量,都被称作常量成员。
在转译时,通过被计算的常量枚举表达式定义值的成员,也被称作常量成员,比如如下几种情况:
引用来自预先定义的常量成员,比如来自当前枚举或其他枚举;
圆括弧 () 包裹的常量枚举表达式;
在常量枚举表达式上应用的一元操作符 +、 -、~ ;
操作常量枚举表达式的二元操作符 +、-、*、/、%、<<、>>、>>>、&、|、^。
除以上这些情况之外,其他都被认为是计算(值)成员。 如下所示示例中,除了 G 是计算成员之外,其他都属于常量成员。
enum FileAccess {
// 常量成员
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 计算成员
G = "123".length,
}
2
3
4
5
6
7
8
9
# 枚举成员类型和联合枚举
对于不需要计算(值)的常量类型成员,即缺省值(从 0 递增)、数字字面量、字符串字面量这三种情况(这就是为什么我们只需记住这三种情况),被称之为字面量枚举成员。
枚举值和类型是一体的,枚举成员的类型是枚举类型的子类型。
枚举成员和枚举类型之间的关系分两种情况: 如果枚举的成员同时包含字面量和非字面量枚举值,枚举成员的类型就是枚举本身(枚举类型本身也是本身的子类型);如果枚举成员全部是字面量枚举值,则所有枚举成员既是值又是类型,如下代码所示:
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 不能作为类型
2
3
4
5
6
7
8
9
10
11
这里因为 Day 的所有成员都是字面量枚举成员,所以 Day.MONDAY 可以同时作为值和类型使用(第 10 行)。但是 MyDay 的成员 MONDAY 是非字面量枚举成员(但是是常量枚举成员),所以 MyDay.MONDAY 仅能作为值使用(第 12 行 ok,第 13 行提示错误)。
另外,如果枚举仅有一个成员且是字面量成员,那么这个成员的类型等于枚举类型,如下代码所示:
enum Day {
MONDAY
}
export const mondayIsDay: Day = Day.MONDAY; // ok
export const mondayIsDay1: Day.MONDAY = mondayIsDay as Day; // ok
2
3
4
5
因为枚举 Day 仅包含一个字面量成员 MONDAY,所以类型 Day 和 Day.MONDAY 可以互相兼容。比如第 4 行和第 5 行,我们既能把 Day.MONDAY 类型赋值给 Day 类型,也能把 Day 类型赋值给 Day.MONDAY 类型。
# 常量枚举(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';
}
}
2
3
4
5
6
7
8
9
10
11
12
这里我们定义了常量枚举 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';
}
};
2
3
4
5
6
7
8
从以上示例我们可以看到,使用常量枚举不仅能减少转译后的 JavaScript 代码量(因为抹除了枚举定义),还不需要到上级作用域里查找枚举定义(因为直接内联了枚举值字面量)。
因此,通过定义常量枚举,我们可以以清晰、结构化的形式维护相关联的常量集合,比如 switch case分支,使得代码更具可读性和易维护性。而且因为转译后抹除了定义、内联成员值,所以在代码的体积和性能方面并不会比直接内联常量值差。
# 外部枚举(Ambient enums)
在 TypeScript 中,我们可以通过 declare 描述一个在其他地方已经定义过的变量,如下代码所示:
declare let $: any;
$('#id').addClass('show'); // ok
2
第 1 行我们使用 declare 描述类型是 any 的外部变量 $,在第 2 行则立即使用 $ ,此时并不会提示一个找不到 $ 变量的错误。
同样,我们也可以使用 declare 描述一个在其他地方已经定义过的枚举类型,通过这种方式定义出来的枚举类型,被称之为外部枚举,如下代码所示:
declare enum Day {
SUNDAY,
MONDAY,
}
const work = (x: Day) => {
if (x === Day.SUNDAY) {
x; // 类型是 Day
}
}
2
3
4
5
6
7
8
9
这里我们认定在其他地方已经定义了一个 Day 这种结构的枚举,且 work 函数中使用了它。
转译为 JavaScript 之后,外部枚举的定义也会像常量枚举一样被抹除,但是对枚举成员的引用会被保留(第 2 行保留了对 Day.SUNDAY 的引用),如下代码所示:
var work = function (x) {
if (x === Day.SUNDAY) {
x;
}
};
2
3
4
5
外部枚举和常规枚举的差异在于以下几点:
在外部枚举中,如果没有指定初始值的成员都被当作计算(值)成员,这跟常规枚举恰好相反;
即便外部枚举只包含字面量成员,这些成员的类型也不会是字面量成员类型,自然完全不具备字面量类型的各种特性。
我们可以一起使用 declare 和 const 定义外部常量枚举,使得它转译为 JavaScript 之后仍像常量枚举一样。在抹除枚举定义的同时,我们可以使用内联枚举值替换对枚举成员的引用。
外部枚举的作用在于为两个不同枚举(实际上是指向了同一个枚举类型)的成员进行兼容、比较、被复用提供了一种途径,这在一定程度上提升了枚举的可用性,让其显得不那么“鸡肋”。