枚举类型

有时候,程序需要根据不同的取值来执行不同的代码,如果直接使用数值类型变量或字符串类型变量的值来做判断,代码的可读性会很差,让人难以记住其含义,而这些值也散落在代码各处,很难统一管理和维护,示例代码如下。

if(userType==1){
    //...
}
else if(userType==2 || userType==3){
    //...
}
else if(userType==4){
    //...
}

此时就适合使用枚举(enum)类型来代替数值类型或字符串类型。枚举类型变量通常用于集中定义和管理一组相关的常量,便于在其他地方引用,从而提高代码的可读性和可维护性。TypeScript 中支持数值枚举和字符串枚举。

数值枚举

数值枚举是数值类型的子类型,是默认的枚举类型,其声明示例如下。

enum MonthOfYear {
    January,
    February,
    March,
    April,
    May,
    ...//省略后续代码
}

let month: MonthOfYear = MonthOfYear.March;
console.log(month);

在示例代码中,首先定义一个名为 MonthOfYear 的枚举,MonthOfYear 中集中维护与月份相关的各个枚举成员;然后定义一个名为 month 的变量,它的类型为 MonthOfYear,初始值为 MonthOfYear.March;最后通过 console.log() 函数将其输出到控制台,输出结果如下。

> 2

在没有显式地给各个枚举成员赋值的情况下,枚举中的第一个成员将从 0 开始取值,而下一个成员会在上一个成员取值的基础上加 1。当不在乎各成员的取值时,使用这种自增长的方式可以让代码显得更精简。但我们也可以显式地给各个成员赋值,例如,修改上述的枚举定义。

enum MonthOfYear {
    January = 1,
    February = 2,
    March,
    April,
    May,
    ...//省略后续代码
}

此时执行示例代码,输出 MonthOfYear.March 的值,会发现该枚举成员的取值是在上一个成员取值的基础上加 1,输出结果如下。

> 3

对于分支判断,如果使用枚举,可以很好地解决可读性和可维护性的问题,例如,定义以下枚举。

enum UserType{
    Admin,
    VIP,
    Normal,
    Guest
}

当做分支判断时,就可以写为如下代码。

if(userType==UserType.Admin){
    //...
}
else if(userType==UserType.VIP || userType==UserType.Normal){
    //...
}
else if(userType==UserType.Guest){
    //...
}

字符串枚举

字符串枚举的定义方式和数值枚举类似,但区别在于各个成员需要显式地赋值为字符串,示例代码如下。

enum MonthOfYear {
    January = "1月",
    February = "2月",
    March = "3月",
    April = "4月",
    May = "5月",
    ...//省略后续代码
}

let month: MonthOfYear = MonthOfYear.March;
console.log(month);

输出结果如下。

> 3月

应慎用的枚举使用方式

虽然善用枚举能够提升代码的可读性和可维护性,但如果误用枚举,则会使得代码的可读性和可维护性都更糟。以下都是 TypeScript 本身支持但应慎用的枚举使用方式。

异构枚举

从技术角度来说,我们可以同时将枚举各成员定义为数值类型和字符串类型,这种混合了两种类型的枚举称为异构枚举,但不推荐这样使用枚举,示例代码如下。

enum MonthOfYear {
    January = 1,
    February = "2月",
    March = 3,
    April = "4月",
    May = 5,
    ...//省略后续代码
}

声明合并

TypeScript 支持将枚举成员先拆分后定义,由于 TypeScript 拥有声明合并的特性,因此它们将合并为一个枚举。例如,以下代码定义了一个名为 Answer 的枚举,把枚举成员 yesno 拆开并单独进行定义。虽然最终两个枚举成员 Answer.noAnswer.yes 都可以访问,但从可维护性的角度来看,这样的情况应当避免。

enum Answer {
    no = 0
}

enum Answer {
    yes = 1
}

let a1: Answer = Answer.yes;
let a2: Answer = Answer.no;

索引查找

要访问具体的枚举成员,通常以 “枚举名称.成员名称” 的方式实现。TypeScript 还支持索引形式,即以 “枚举名称[含有成员名称的字符串变量]” 的方式访问具体成员。除非在极其特殊的情况下,否则不推荐使用索引查找。示例代码如下。

enum Answer {
    no,
    yes
}

let inputString: string = "yes";
let userAnswer: Answer = Answer[inputString];
console.log(userAnswer);

输出结果如下。

> 1

以上代码定义了一个变量 inputString,其值为 yes,然后通过索引访问具体枚举成员。从功能上来说这没什么问题,但它会绕过 TypeScript 的编译检查,一旦变量值有误,程序将无法正常运行。例如,如果修改上述代码中 inputString 的定义部分,将其改为以下代码。

...
let inputString: string = "YES";
...

代码看似没有问题,但 yes 变成了 YES,因此无法检索到对应的成员。在编译 TypeScript 代码时无法检测出该错误。一旦运行代码,输出结果如下。

> undefined

反向映射

对于数值枚举,通过反向映射 “枚举名称[枚举成员” 的方式返回枚举成员的名称。除非在极其特殊的情况,否则也不推荐这种使用方式,示例代码如下。

enum Answer {
    no,
    yes,
}

let nameOfyes: string = Answer[Answer.yes];
console.log(nameOfyes);

输出结果如下。

> yes

注意,字符串枚举无法使用反向映射,“枚举名称[字符串枚举成员]” 的返回结果为undefined

常量枚举

如果使用普通的数值枚举或字符串枚举,在编译成 JavaScript 代码后会产生较多代码来支持各项功能,开销较大且可读性较差,而且很可能被人误用。例如,使用索引查找或反向映射会导致可读性进一步变差,出错率进一步提高。

要解决上述问题,使用常量枚举。要定义常量枚举,只需在普通枚举的定义前面加上 const 关键字,示例代码如下。

const enum Answer {
    no,
    yes,
}

let actualAnswer: Answer = Answer.yes;

此时如果编译这段代码,你可以发现它与普通枚举编译后产生的 JavaScript 代码存在区别。以下是普通枚举编译后产生的 JavaScript 代码。

var Answer;
(function (Answer) {
    Answer[Answer["no"] = 0] = "no";
    Answer[Answer["yes"] = 1] = "yes";
})(Answer || (Answer = {}));
var actualAnswer = Answer.yes;

以下是常量枚举编译后产生的 JavaScript 代码,整体上更精简。

var actualAnswer = 1 /* yes */;

如果对常量枚举使用索引查找或反向映射,编译将无法通过,示例代码如下。

let inputString: string = "yes";
//编译错误:只有使用字符串文本才能访问常数枚举成员。ts(2476)
let userAnswer: Answer = Answer[inputString];
//编译错误:只有使用字符串文本才能访问常数枚举成员。ts(2476)
let nameOfyes: string = Answer[Answer.yes];