关注「索引目录」公众号,获取更多干货。
三个月前,我提交了一个我认为非常合理的拉取请求。我创建了一个新的UserRole枚举来处理我们的权限系统。简洁、类型安全、符合 TypeScript 规范。
高级工程师的评审结果只有一个:“请不要使用枚举。”
我当时很困惑。枚举在 TypeScript 手册里,每门课程都会讲到。主流代码库都在用它们。枚举有什么问题吗?
然后他向我展示了编译后的 JavaScript 输出。
那天下午我从我们的代码库中删除了每个枚举。
本文解释了为什么 TypeScript 枚举是该语言最容易被误解的功能之一,以及为什么你应该停止使用它们。
第一部分:枚举错觉
TypeScript 自称是“具有类型语法的 JavaScript”。它的承诺很简单:编写 TypeScript,获得类型安全,编译为干净的 JavaScript。
对于大多数 TypeScript 特性来说,确实如此。接口?被删除了。类型注解?被删除了。泛型?被删除了。
枚举?它们会变成真正的运行时代码。
这种根本的区别使得枚举在 TypeScript 中成为一个异常现象,并且对于不了解编译模型的开发人员来说是一个陷阱。
简单的例子
让我们从一些无辜的事情开始:
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING"
}
function getUserStatus(): Status {
return Status.Active
}
看起来很简洁,对吧?以下是实际交付给用户的内容:
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
Status["Pending"] = "PENDING";
})(Status || (Status = {}));
function getUserStatus() {
return Status.Active;
}
也就是说,5 行 TypeScript 代码对应9 行 JavaScript 代码。
但等等——情况变得更糟了。
第 2 部分:数字枚举的噩梦
字符串枚举很糟糕。数字枚举更是一场灾难。
enum Role {
Admin,
User,
Guest
}
你可能以为这会编译成一些简单的东西。也许吧const Role = { Admin: 0, User: 1, Guest: 2 }。
以下是您实际得到的:
var Role;
(function (Role) {
Role[Role["Admin"] = 0] = "Admin";
Role[Role["User"] = 1] = "User";
Role[Role["Guest"] = 2] = "Guest";
})(Role || (Role = {}));
这里发生了什么事?
TypeScript 正在创建反向映射。编译后的对象如下所示:
{
Admin: 0,
User: 1,
Guest: 2,
0: "Admin",
1: "User",
2: "Guest"
}
这允许您执行以下操作:Role[0] // "Admin"
问题:您是否曾经需要过此功能?
在五年的专业 TypeScript 开发经验中,我从来没有需要通过枚举值的数值来查找枚举名称。一次也没有。
然而我已经将这段额外的代码投入生产数百次了。
第三部分:Tree-Shaking 问题
Webpack、Rollup 和 Vite 等现代打包工具都拥有先进的 tree-shaking 功能,能够精准地删除无用代码。
除非您使用枚举。
问题
// types.ts
export enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
Archived = "ARCHIVED",
Deleted = "DELETED"
}
// app.ts
import { Status } from './types'
const currentStatus = Status.Active
您想要的:"ACTIVE"只是捆绑包中的字符串。
您得到的是:整个Status枚举对象加上 IIFE 包装器。
枚举无法进行 tree-shaking,因为它们是运行时构造的。即使你只使用一个值,你也会获取所有值。
在实际应用程序中将其乘以数十个枚举,您将发送数千字节的不必要代码。
第四部分:更好的选择
那么如果枚举有问题,我们应该用什么来代替呢?
解决方案 1:使用 'as const' 的 Const 对象
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE",
Pending: "PENDING"
} as const
编译后的 JavaScript:
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE",
Pending: "PENDING"
}
就是这样。没有 IIFE。没有运行时开销。只是一个简单的对象。
创建类型
type Status = typeof Status[keyof typeof Status]
// Expands to: type Status = "ACTIVE" | "INACTIVE" | "PENDING"
现在你有:
-
✅ 值的运行时对象 -
✅ 用于类型检查的编译时类型 -
✅ 零编译开销 -
✅ 可摇树(如果你的打包工具支持的话)
用法
// Works exactly like enums:
function setStatus(status: Status) {
console.log(status)
}
setStatus(Status.Active) // ✅ Valid
setStatus("ACTIVE") // ✅ Valid (it's just a string)
setStatus("INVALID") // ❌ Type error
第五部分:类型安全优势
有趣的是:const 对象比枚举提供更好的类型安全性。
枚举问题
enum Color {
Red = 0,
Blue = 1
}
enum Status {
Inactive = 0,
Active = 1
}
function setColor(color: Color) {
console.log(`Color: ${color}`)
}
// This compiles successfully:
setColor(Status.Active) // No error!
为什么?因为 TypeScript 枚举使用结构化类型。Color和都是Status数字,所以 TypeScript 认为它们兼容。
这段代码编译并发布到生产环境。但它引发了一个 bug,需要几个小时才能调试。
对象解决方案
const Color = {
Red: "RED",
Blue: "BLUE"
} as const
const Status = {
Inactive: "INACTIVE",
Active: "ACTIVE"
} as const
type Color = typeof Color[keyof typeof Color]
function setColor(color: Color) {
console.log(`Color: ${color}`)
}
// Type error:
setColor(Status.Active) // ❌ Type '"ACTIVE"' is not assignable to type '"RED" | "BLUE"'
const 对象方法使用文字类型,即精确的字符串值。TypeScript 在编译时捕获错误。
Const 对象提供比枚举更严格的类型检查。
第 6 部分:迁移路径
相信了吗?下面是如何迁移现有枚举的方法。
步骤 1:识别字符串枚举
这些是最容易迁移的:
// Before
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE"
}
// After
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE"
} as const
type Status = typeof Status[keyof typeof Status]
步骤 2:转换数字枚举
对于数字枚举,您需要保留数字:
// Before
enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500
}
// After
const HttpStatus = {
OK: 200,
NotFound: 404,
ServerError: 500
} as const
type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]
步骤 3:更新使用情况
好消息?用法基本保持不变:
// Both work identically:
const status1: Status = Status.Active
const status2: HttpStatus = HttpStatus.OK
// Pattern matching still works:
switch (status) {
case Status.Active:
// ...
case Status.Inactive:
// ...
}
步骤 4:处理边缘情况
如果您使用反向查找(很少见),则需要创建一个明确的反向映射:
const HttpStatus = {
OK: 200,
NotFound: 404
} as const
// Create reverse mapping only if needed:
const HttpStatusNames = {
200: "OK",
404: "NotFound"
} as const
HttpStatusNames[200] // "OK"
第七部分:唯一的例外
使用枚举是否有正当理由?
可能:const 枚举
const enum Direction {
Up,
Down,
Left,
Right
}
const move = Direction.Up
编译为:
const move = 0 /* Direction.Up */
常量枚举在编译时内联。它们不会创建运行时对象。
然而:
-
它们不适用于 isolatedModules(Babel、esbuild、SWC 所需) -
他们被弃用,取而代之的是 preserveConstEnums -
它们比仅仅使用对象更复杂
我的建议:即使是 const 枚举,也只使用对象。越简单越好。
第 8 部分:现实世界的影响
当我们将代码库从枚举迁移到 const 对象时,发生了以下情况:
迁移之前
- 代码库中的枚举:
47 - 捆绑包大小:
2.4 MB(最小化) - 捆绑包中与枚举相关的代码:
~14 KB
迁移后
- 代码库中的枚举:
0 - 捆绑包大小:
2.388 MB(最小化) - 节省:
12 KB
“只有12KB?”
是的,但是:
-
12KB 的数据我们不需要发送、解析或执行 -
类型安全性得到改善(我们在迁移过程中发现了 3 个错误) -
代码变得更易读(它只是 JavaScript) -
新开发人员上手更快(TypeScript 怪癖更少)
开发者体验改进
- 更快的编译:
TypeScript 不需要生成枚举代码 - 更好的 IDE 性能:
需要跟踪的运行时构造更少 - 更容易调试:
控制台日志显示实际值,而不是枚举引用 - 更简单的思维模型:
少记住一个 TypeScript 特有的功能
第九部分:常见反对意见
“但是枚举在 TypeScript 文档中!”
命名空间也是如此,它们也被视为遗留。TypeScript 团队已经承认枚举是一个错误,但他们无法在不破坏变更的情况下删除它们。
“我的整个代码库都使用枚举!”
迁移过程简单直接,可以逐步完成。从新代码开始,在重构过程中迁移旧代码。
“枚举更加明确!”
// Enum
enum Status { Active = "ACTIVE" }
// Object
const Status = { Active: "ACTIVE" } as const
区别很小。对象版本实际上更符合 JavaScript 的惯用用法。
“我需要类型和值!”
使用 const 对象模式可以同时获得这两种效果:
const Status = { Active: "ACTIVE" } as const // Runtime value
type Status = typeof Status[keyof typeof Status] // Compile-time type
“JSON 序列化怎么样?”
无论如何,枚举都会序列化为其底层值:
enum Status { Active = "ACTIVE" }
JSON.stringify({ status: Status.Active }) // {"status":"ACTIVE"}
相同于:
const Status = { Active: "ACTIVE" } as const
JSON.stringify({ status: Status.Active }) // {"status":"ACTIVE"}
没有区别。
第十部分:哲学观点
TypeScript 的座右铭是“可扩展的 JavaScript”。最好的 TypeScript 代码是看起来像 JavaScript 但带有类型注释的代码。
枚举违反了这一原则。它们是一种仅适用于 TypeScript 的结构,会生成运行时代码,并且其行为与 JavaScript 中的任何内容都不同。
如有疑问,请优先使用具有 TypeScript 类型的 JavaScript 习语,而不是 TypeScript 特定的功能。
好的 TypeScript:
const Status = { Active: "ACTIVE" } as const
type Status = typeof Status[keyof typeof Status]
这是具有 TypeScript 类型的 JavaScript(一个对象)。它可扩展。它很熟悉。它在任何地方都能工作。
可疑的 TypeScript:
enum Status { Active = "ACTIVE" }
这是 TypeScript 特定的语法,会生成意外的运行时代码。
结论:做出改变
TypeScript 枚举在 2012 年似乎是个好主意。到 2025 年,我们将有更好的选择。
反对枚举的情况:
-
❌ 生成意外的运行时代码 -
❌ 不要摇晃树 -
❌ 创建没人使用的反向映射 -
❌ 类型安全性比字面量类型弱 -
❌ TypeScript 特定的语法
const 对象的情况:
-
✅ 零运行时开销 -
✅ 可摇树 -
✅ 仅 JavaScript -
✅ 更强的类型安全性 -
✅ 随处可用
下次使用枚举时,请改用 const 对象。
你的包会更小。你的类型会更严格。你的代码会更清晰。
停止使用枚举。开始使用对象。
快速参考指南
字符串枚举迁移
// ❌ Old way
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE"
}
// ✅ New way
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE"
} as const
type Status = typeof Status[keyof typeof Status]
数字枚举迁移
// ❌ Old way
enum Priority {
Low = 1,
Medium = 2,
High = 3
}
// ✅ New way
const Priority = {
Low: 1,
Medium: 2,
High: 3
} as const
type Priority = typeof Priority[keyof typeof Priority]
可重用性的辅助类型
// Create a reusable type helper
type ValueOf<T> = T[keyof T]
const Status = {
Active: "ACTIVE",
Inactive: "INACTIVE"
} as const
type Status = ValueOf<typeof Status>
关注「索引目录」公众号,获取更多干货。

