TypeScript 枚举(Enum)

在真实项目开发中,小伙伴们肯定都遇到过这样一种场景:我们需要在代码里判断一个订单的状态。

通常情况下,后端一般会提前和我们约定好: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 依然是非常优秀且被广泛使用的。大家可以根据团队规范来灵活选择。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号