- Published on
第十三章 TypeScript 的 Enum 类型
- Authors
- Name
- Tian Haipeng
简介
实际开发中,经常需要定义一组相关的常量。
const RED = 1
const GREEN = 2
const BLUE = 3
let color = userInput()
if (color === RED) {
/* */
}
if (color === GREEN) {
/* */
}
if (color === BLUE) {
/* */
}
throw new Error('wrong color')
上面示例中,常量RED
、GREEN
、BLUE
是相关的,意为变量color
的三个可能的取值。它们具体等于什么值其实并不重要,只要不相等就可以了。
TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}
上面示例声明了一个 Enum 结构Color
,里面包含三个成员Red
、Green
和Blue
。第一个成员的值默认为整数0
,第二个为1
,第二个为2
,以此类推。
使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。
let c = Color.Green // 1
// 等同于
let c = Color['Green'] // 1
Enum 结构本身也是一种类型。比如,上例的变量c
等于1
,它的类型可以是 Color,也可以是number
。
let c: Color = Color.Green // 正确
let c: number = Color.Green // 正确
上面示例中,变量c
的类型写成Color
或number
都可以。但是,Color
类型的语义更好。
Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。
// 编译前
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}
// 编译后
let Color = {
Red: 0,
Green: 1,
Blue: 2,
}
上面示例是 Enum 结构编译前后的对比。
由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。
Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
enum Operator {
ADD,
DIV,
MUL,
SUB,
}
function compute(op: Operator, a: number, b: number) {
switch (op) {
case Operator.ADD:
return a + b
case Operator.DIV:
return a / b
case Operator.MUL:
return a * b
case Operator.SUB:
return a - b
default:
throw new Error('wrong operator')
}
}
compute(Operator.ADD, 1, 3) // 4
上面示例中,Enum 结构Operator
的四个成员表示四则运算“加减乘除”。代码根本不需要用到这四个成员的值,只用成员名就够了。
Enum 作为类型有一个缺点,就是输入任何数值都不报错。
enum Bool {
No,
Yes,
}
function foo(noYes: Bool) {
// ...
}
foo(33) // 不报错
上面代码中,函数foo
的参数noYes
只有两个可用的值,但是输入任意数值,编译都不会报错。
另外,由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)。
enum Color {
Red,
Green,
Blue,
}
const Color = 'red' // 报错
上面示例,Enum 结构与变量同名,导致报错。
很大程度上,Enum 结构可以被对象的as const
断言替代。
enum Foo {
A,
B,
C,
}
const Bar = {
A: 0,
B: 1,
C: 2,
} as const;
if (x === Foo.A){}
// 等同于
if (x === Bar.A) {}
上面示例中,对象Bar
使用了as const
断言,作用就是使得它的属性无法修改。这样的话,Foo
和Bar
的行为就很类似了,前者完全可以用后者替代,而且后者还是 JavaScript 的原生数据结构。
Enum 成员的值
Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如 0、1、2……
但是,也可以为 Enum 成员显式赋值。
enum Color {
Red,
Green,
Blue,
}
// 等同于
enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
上面示例中,Enum 每个成员的值都是显式赋值。
成员的值可以是任意数值,但不能是大整数(Bigint)。
enum Color {
Red = 90,
Green = 0.5,
Blue = 7n, // 报错
}
上面示例中,Enum 成员的值可以是小数,但不能是 Bigint。
成员的值甚至可以相同。
enum Color {
Red = 0,
Green = 0,
Blue = 0,
}
如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
enum Color {
Red = 7,
Green, // 8
Blue, // 9
}
// 或者
enum Color {
Red, // 0
Green = 7,
Blue, // 8
}
Enum 成员的值也可以使用计算式。
enum Permission {
UserRead = 1 << 8,
UserWrite = 1 << 7,
UserExecute = 1 << 6,
GroupRead = 1 << 5,
GroupWrite = 1 << 4,
GroupExecute = 1 << 3,
AllRead = 1 << 2,
AllWrite = 1 << 1,
AllExecute = 1 << 0,
}
enum Bool {
No = 123,
Yes = Math.random(),
}
上面示例中,Enum 成员的值等于一个计算式,或者等于函数的返回值,都是正确的。
Enum 成员值都是只读的,不能重新赋值。
enum Color {
Red,
Green,
Blue,
}
Color.Red = 4 // 报错
上面示例中,重新为 Enum 成员赋值就会报错。
为了让这一点更醒目,通常会在 enum 关键字前面加上const
修饰,表示这是常量,不能再次赋值。
const enum Color {
Red,
Green,
Blue,
}
加上const
还有一个好处,就是编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,这样能提高性能表现。
const enum Color {
Red,
Green,
Blue,
}
const x = Color.Red
const y = Color.Green
const z = Color.Blue
// 编译后
const x = 0 /* Color.Red */
const y = 1 /* Color.Green */
const z = 2 /* Color.Blue */
上面示例中,由于 Enum 结构前面加了const
关键字,所以编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。
如果希望加上const
关键词后,运行时还能访问 Enum 结构(即编译后依然将 Enum 转成对象),需要在编译时打开preserveConstEnums
编译选项。
同名 Enum 的合并
多个同名的 Enum 结构会自动合并。
enum Foo {
A,
}
enum Foo {
B = 1,
}
enum Foo {
C = 2,
}
// 等同于
enum Foo {
A,
B = 1,
C = 2
}
上面示例中,Foo
分成三段定义,系统会自动把它们合并。
Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
enum Foo {
A,
}
enum Foo {
B, // 报错
}
上面示例中,Foo
的两段定义的第一个成员,都没有设置初始值,导致报错。
同名 Enum 合并时,不能有同名成员,否则报错。
enum Foo {
A,
B,
}
enum Foo {
B = 1, // 报错
C,
}
上面示例中,Foo
的两段定义有一个同名成员B
,导致报错。
同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。
// 正确
enum E {
A,
}
enum E {
B = 1,
}
// 正确
const enum E {
A,
}
const enum E {
B = 1,
}
// 报错
enum E {
A,
}
const enum E {
B = 1,
}
同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。
字符串 Enum
Enum 成员的值除了设为数值,还可以设为字符串。也就是说,Enum 也可以用作一组相关字符串的集合。
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
上面示例中,Direction
就是字符串枚举,每个成员的值都是字符串。
注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。
enum Foo {
A, // 0
B = 'hello',
C, // 报错
}
上面示例中,A
之前没有其他成员,所以可以不设置初始值,默认等于0
;C
之前有一个字符串成员,所以C
必须有初始值,不赋值就报错了。
Enum 成员可以是字符串和数值混合赋值。
enum Enum {
One = 'One',
Two = 'Two',
Three = 3,
Four = 4,
}
除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。
变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。
enum MyEnum {
One = 'One',
Two = 'Two',
}
let s = MyEnum.One
s = 'One' // 报错
上面示例中,变量s
的类型是MyEnum
,再赋值为字符串就报错。
由于这个原因,如果函数的参数类型是字符串 Enum,传参时就不能直接传入字符串,而要传入 Enum 成员。
enum MyEnum {
One = 'One',
Two = 'Two',
}
function f(arg: MyEnum) {
return 'arg is ' + arg
}
f('One') // 报错
上面示例中,参数类型是MyEnum
,直接传入字符串会报错。
所以,字符串 Enum 作为一种类型,有限定函数参数的作用。
前面说过,数值 Enum 的成员值往往不重要。但是有些场合,开发者可能希望 Enum 成员值可以保存一些有用的信息,所以 TypeScript 才设计了字符串 Enum。
const enum MediaTypes {
JSON = 'application/json',
XML = 'application/xml',
}
const url = 'localhost'
fetch(url, {
headers: {
Accept: MediaTypes.JSON,
},
}).then((response) => {
// ...
})
上面示例中,函数fetch()
的参数对象的属性Accept
,只能接受一些指定的字符串。这时就很适合把字符串放进一个 Enum 结构,通过成员值来引用这些字符串。
字符串 Enum 可以使用联合类型(union)代替。
function move(where: 'Up' | 'Down' | 'Left' | 'Right') {
// ...
}
上面示例中,函数参数where
属于联合类型,效果跟指定为字符串 Enum 是一样的。
注意,字符串 Enum 的成员值,不能使用表达式赋值。
enum MyEnum {
A = 'one',
B = ['T', 'w', 'o'].join(''), // 报错
}
上面示例中,成员B
的值是一个字符串表达式,导致报错。
keyof 运算符
keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。
enum MyEnum {
A = 'a',
B = 'b',
}
// 'A'|'B'
type Foo = keyof typeof MyEnum
上面示例中,keyof typeof MyEnum
可以取出MyEnum
的所有成员名,所以类型Foo
等同于联合类型'A'|'B'
。
注意,这里的typeof
是必需的,否则keyof MyEnum
相当于keyof number
。
type Foo = keyof MyEnum
// "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
上面示例中,类型Foo
等于类型number
的所有原生属性名组成的联合类型。
这是因为 Enum 作为类型,本质上属于number
或string
的一种变体,而typeof MyEnum
会将MyEnum
当作一个值处理,从而先其转为对象类型,就可以再用keyof
运算符返回该对象的所有属性名。
如果要返回 Enum 所有的成员值,可以使用in
运算符。
enum MyEnum {
A = 'a',
B = 'b',
}
// { a:any, b: any }
type Foo = { [key in MyEnum]: any }
上面示例中,采用属性索引可以取出MyEnum
的所有成员值。
反向映射
数值 Enum 存在反向映射,即可以通过成员值获得成员名。
enum Weekdays {
Monday = 1,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
console.log(Weekdays[3]) // Wednesday
上面示例中,Enum 成员Wednesday
的值等于 3,从而可以从成员值3
取到对应的成员名Wednesday
,这就叫反向映射。
这是因为 TypeScript 会将上面的 Enum 结构,编译成下面的 JavaScript 代码。
var Weekdays
;(function (Weekdays) {
Weekdays[(Weekdays['Monday'] = 1)] = 'Monday'
Weekdays[(Weekdays['Tuesday'] = 2)] = 'Tuesday'
Weekdays[(Weekdays['Wednesday'] = 3)] = 'Wednesday'
Weekdays[(Weekdays['Thursday'] = 4)] = 'Thursday'
Weekdays[(Weekdays['Friday'] = 5)] = 'Friday'
Weekdays[(Weekdays['Saturday'] = 6)] = 'Saturday'
Weekdays[(Weekdays['Sunday'] = 7)] = 'Sunday'
})(Weekdays || (Weekdays = {}))
上面代码中,实际进行了两组赋值,以第一个成员为例。
Weekdays[(Weekdays['Monday'] = 1)] = 'Monday'
上面代码有两个赋值运算符(=
),实际上等同于下面的代码。
Weekdays['Monday'] = 1
Weekdays[1] = 'Monday'
注意,这种情况只发生在数值 Enum,对于字符串 Enum,不存在反向映射。这是因为字符串 Enum 编译后只有一组赋值。
enum MyEnum {
A = 'a',
B = 'b',
}
// 编译后
var MyEnum
;(function (MyEnum) {
MyEnum['A'] = 'a'
MyEnum['B'] = 'b'
})(MyEnum || (MyEnum = {}))