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. 如下图
解析: 按照官方文档的解释, 先判断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 的外部变量 ,此时并不会提示一个找不到 $ 变量的错误。
同样,我们也可以使用 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 之后仍像常量枚举一样。在抹除枚举定义的同时,我们可以使用内联枚举值替换对枚举成员的引用。
外部枚举的作用在于为两个不同枚举(实际上是指向了同一个枚举类型)的成员进行兼容、比较、被复用提供了一种途径,这在一定程度上提升了枚举的可用性,让其显得不那么“鸡肋”。