在真实项目开发中,小伙伴们肯定都遇到过这样一种场景:我们需要在代码里判断一个订单的状态。
通常情况下,后端一般会提前和我们约定好:0 代表 “未支付”、1 代表 “已支付”、2 代表 “已发货”。于是,我们在代码里就会写出下面这样的逻辑:
// 假设 status 从后端接口获取
const status = 1;
if (status === 0) {
console.log("订单未支付");
} else if (status === 1) {
console.log("订单已支付");
}上面这段代码现在看起来没啥问题。但是过了几个月后,或者换个新同事接手,看到 if (status === 1) 绝对会一脸懵逼:“这个 1 到底是什么意思?”
这种在代码中突然出现、让人摸不着头脑的数字,在编程界被称为 “魔法数字(Magic Number)”。为了消灭魔法数字,提高代码的可读性和可维护性,TypeScript 引入了一个非常经典的概念:枚举(Enum)。
TypeScript 枚举是什么?
所谓的 “枚举(Enum)”,指的就是把一组相关的常量组织在一起,并且给它们起一个有意义的名字。
在 TypeScript 中,我们可以使用 enum 关键字来定义枚举类型。其中,枚举主要分为两种:数字枚举、字符串枚举和异构枚举。
1. 数字枚举(默认)
如果我们只写枚举的名字,不给它们赋值,则 TypeScript 会默认它们是 “数字枚举”,并且会从 0 开始自动给它们编号。
语法:
enum 枚举名 {
成员1,
成员2,
...
}示例 1:定义数字枚举
// 定义枚举
enum OrderStatus {
Unpaid, // 默认是 0
Paid, // 自动递增为 1
Shipped // 自动递增为 2
}
// 使用枚举
const currentStatus: OrderStatus = OrderStatus.Paid;
console.log(currentStatus);运行结果如下。
1分析:
在这个例子中,OrderStatus.Paid 的底层值其实就是数字 1。但我们在写代码的时候,用到的是 OrderStatus.Paid 这个更具语义化的名字。别人一看就知道这是 “已支付” 的状态,非常的直观。
此外,数字枚举还有一个有用的特性:自定义起点。如果我们不希望从 0 开始,此时可以手动给第一个成员赋值,然后后面的成员会自动递增。
示例 2:自定义枚举数字
enum OrderStatus {
Unpaid = 10, // 手动赋值为 10
Paid, // 自动变成 11
Shipped // 自动变成 12
}
console.log(OrderStatus.Shipped);运行结果如下。
12分析:
在这个例子中,我们手动将第一个成员的值赋为 10,然后后面的成员就会自动变成 11、12等。
2. 字符串枚举
虽然数字枚举很好用,但在某些业务场景下(比如前端调试、或者接口明确要求传字符串时),我们更希望枚举的值是具有可读性的字符串。此时就需要用到 TypeScript 中的 “字符串枚举” 了。
示例 3:自定义字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
const userDirection: Direction = Direction.Up;
console.log(userDirection);运行结果如下。
UP分析:
需要注意的是,TypeScript 字符串枚举没有 “自动递增” 的特性,因此我们必须手动为每一个枚举成员赋值。
使用字符串枚举的好处是:当我们使用 console.log() 打印这个变量时,输出的是清晰明了的字符串 "UP",而不是干巴巴的数字。
3. 异构枚举(不推荐)
除了数字枚举和字符串枚举之外,我们还可以将数字和字符串混合在一个枚举中,这种在 TypeScript 也被称为 “异构枚举”。
示例 4:使用异构枚举
// 定义一个异构枚举,里面既有数字,又有字符串
enum BooleanLike {
No = 0,
Yes = "YES"
}
const myAnswer: BooleanLike = BooleanLike.Yes;
console.log(myAnswer);运行结果如下。
YES分析:
需要注意的是,除非你正在重构古怪的 “祖传” JavaScript 代码,否则在现代前端项目中,强烈建议小伙伴们不要使用异构枚举。因为保持枚举内数据类型的一致性,是良好代码规范的基本要求。
提示: Python 也有 “枚举” 的概念,学过 Python 的小伙伴们可以对比理解一下。
TypeScript 常量枚举
普通的枚举在被 TypeScript 编译成 JavaScript 后,会生成一大坨真实的对象代码。假如项目里有几十个枚举,就会让编译后的代码体积变得非常臃肿。
如果我们仅仅是为了在写代码时获得提示和可读性,并不需要在运行时去遍历这个枚举,此时就可以使用 “常量枚举(const enum)”。
语法:
const enum 枚举名 {
成员1,
成员2,
...
}说明:
我们只需要在 enum 前面加上 const 关键字,该枚举类型就会变成一个常量枚举。
示例 5:使用常量枚举
const enum HttpCode {
Success = 200,
NotFound = 404,
ServerError = 500
}
const currentCode: HttpCode = HttpCode.Success;分析:
加上了 const 之后,上面这段 TypeScript 代码最终会编译成:
const currentCode = 200;也就是说,编译后的 JavaScript 代码只有这一句。枚举对象的定义被完全抹除了,代码变得非常干净。
TypeScript 编译器会在编译阶段,直接把你用到枚举的地方替换成最终的数值。这不仅让代码更加安全,还极致优化了前端打包后的体积。
TypeScript 数字枚举的 “反向映射”
在 TypeScript 的数字枚举中,有一个非常牛逼的特性,叫做 “反向映射”。“反向映射” 其实非常好理解,它指的是:我们不仅可以通过 “属性名” 拿到 “值”,还可以通过 “值” 反向拿到 “属性名”。
示例 6:枚举的反向映射
enum Role {
Admin, // 0
User, // 1
Guest // 2
}
// 正向映射:通过名字拿值
const roleValue: number = Role.User;
console.log(roleValue);
// 反向映射:通过值拿名字
const roleName: string = Role[1];
console.log(roleName);运行结果如下。
1
User分析:
在我们需要根据后端返回的数字状态码,在页面上直接回显出对应英文字段时,此时使用反向映射就非常方便了。
注意: 只有数字枚举才支持反向映射,字符串枚举是不支持的。
在实际开发中,如果遇到固定集合的状态码、类型标识时,我们强烈推荐使用 “枚举(Enum)” 来代替零散的变量声明。如果不需要用到 “反向映射” 等复杂特性,我们强烈建议直接使用 “常量枚举(const enum)”,以追求极致的性能。
拓展:关于社区的 Enum 争议
虽然枚举在管理状态码时非常方便,但在如今的 TypeScript 社区中,普通枚举其实存在一定的争议。主要原因有以下 2 个:
- 它打破了 TypeScript “不增加运行时代码” 的原则,编译后会生成额外的对象代码。
- 数字枚举的反向映射特性,有时会导致类型推导不够严格。
虽然我们可以使用 “常量枚举(const enum)” 来解决体积臃肿的问题,但在很多极致追求纯净度的现代开源库(如 Vue 3 源码)中,大佬们更倾向于使用普通的 JavaScript 对象结合 as const 断言,或者直接使用我们下一章会学到的 “联合类型”。
我们可以使用联合类型,来平替字符串枚举,比如:
type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT";
let dir: Direction = "UP"; // 依然有完美的智能提示这种写法完全没有运行时负担。当然了,小伙伴们也不用有太重的心理负担,在日常的业务开发中,enum 和 const enum 依然是非常优秀且被广泛使用的。大家可以根据团队规范来灵活选择。
