1. 官方是否真的不推荐 Enum?
官方 并没有 直接说不推荐使用 enum,但 TypeScript 团队确实在新版本中推出了一些配置和特性,使得 enum 在某些情况下不再是最佳选择,这导致许多开发者开始讨论 “官方是不是在逐步削弱 enum?”
1.1 事情的起因
TypeScript 5.5 引入了 --erasableSyntaxOnly 这个新的编译选项。
作用是:强制 TypeScript 代码只使用“可擦除”的类型语法,即不产生运行时代码的类型定义。
而 enum 会在编译后的 JavaScript 代码中留下额外的运行时代码,因此 enum 不符合 这个规则,如果启用了 --erasableSyntaxOnly,编译器会报错,禁止使用 enum。
这让很多人误以为 官方不推荐 enum 了,但实际上,官方只是给出了一个选项,并没有强制不让用。
1.2 可擦除语法(Erasable Syntax)是什么?
“可擦除语法” 指的是 在编译时会被完全移除,不会留下任何 JavaScript 代码的 TypeScript 语法。
比如:
// 这些都是 "可擦除" 的
type User = { name: string; age: number }; // 纯 TS 类型
const fn = (value: string): number => value.length; // 返回值类型
而 enum 是不可擦除的,因为它在 JavaScript 运行时代码中仍然会生成对象,例如:
可以看到 enum 变成了一个 IIFE(立即执行函数),在运行时仍然占据空间。
1.3 TS 官方为什么要出 erasableSyntaxOnly?
目的:让 TypeScript 代码更“纯粹”、更轻量。
TypeScript 最核心的作用是“类型检查”,而不是“修改 JavaScript 运行时代码”。
enum 会在编译后生成额外的 JavaScript 代码,而 type、interface 之类的类型定义 在运行时是完全不存在的,所以 --erasableSyntaxOnly 允许开发者强制只使用纯类型,不让 enum 这种会生成运行时代码的特性污染代码。
对比 enum 和 const enum
官方其实早就推荐:优先使用 const enum 而不是 enum,因为 const enum 在编译后不会留下多余的 JS 代码。
这就是 const enum 更推荐的原因。
erasableSyntaxOnly 主要作用:
- 消除运行时代码污染:强制只使用“纯类型”,减少 JS 代码大小,提高运行时性能。
- 确保 TS 代码更接近 JS 语法,不依赖 TS 特有的扩展语法(如 enum)。
- 让 TypeScript 编译后的代码更加可控,更适合 Web 前端优化(比如 Tree Shaking)。
1.4 enum 还能不能用?
可以用,但官方在推 const enum 以及其他替代方案。
如果项目中开启了 --erasableSyntaxOnly,就必须改用 const enum 或其他替代方案(如 union types)。
如果不启用这个选项,依然可以正常使用 enum,但在大部分前端项目中,推荐使用 const enum 或 联合类型 代替,以减少额外的 JS 代码。
官方是否不推荐 Enum 的结论:TypeScript 官方并未明确反对使用 Enum,但社区和部分场景下确实存在更推荐使用联合类型(Union Types)的趋势。
2. Enum 的核心问题
尽管 enum 在 TypeScript 中提供了方便的方式来定义一组有意义的值,但它确实存在一些问题,使其在很多情况下并不是最佳选择。
1. 默认值陷阱:隐式数值分配
1、数字 Enum:未显式赋值的成员自动从 0 递增,易导致意外行为。
enum Status { Open, Closed } // Open=0, Closed=1
console.log(Status[1]); // 输出 "Closed"(反向映射)
如果开发者没有显式指定值,可能会导致意外的值被赋予,尤其是在大型项目中,难以追踪。
2、字符串 Enum:必须显式初始化,无自动推导。
enum Direction {
Up = 'UP',
Down = 'DOWN' // 必须手动定义所有成员
}
2. 类型系统缺陷(设计限制)
1、枚举值类型不精确
enum Color { Red = 'RED' }
type T = Color.Red; // 类型为 Color,而非 'RED'
function logColor(color: 'RED') { }
logColor(Color.Red); // 报错:参数类型不匹配(Color vs 'RED')
本质原因:Enum 是运行时存在的真实对象,而 RED 是纯类型字面量。
2、需强制类型断言
const color: 'RED' = Color.Red as 'RED'; // 需要显式断言
3. 运行时开销
Enum 编译为 IIFE:
// TS 源码
enum Size { Small = 'S', Medium = 'M' }
// 编译后(ES5)
var Size;
(function (Size) {
Size["Small"] = "S";
Size["Medium"] = "M";
})(Size || (Size = {}));
问题:这个额外的 JavaScript 代码会增加 bundle 体积,影响前端项目的加载速度。enum 在运行时仍然是一个 JavaScript 对象,这可能会导致 Tree Shaking 失效,无法优化未使用的 enum 值。
3. 官方态度与 --erasableSyntaxOnly 技术细节
不弃用原因:历史代码兼容性,但明确推荐替代方案。
可擦除语法:通过配置 --erasableSyntaxOnly,在编译时擦除 Enum 的运行时代码,仅保留类型。
// tsconfig.json
{
"compilerOptions": {
"erasableSyntaxOnly": true
}
}
作用:将 Enum 编译为纯类型声明,擦除运行时代码。
// TS 源码
enum Flag { A = 1 }
// 编译后(开启 erasableSyntaxOnly)
// 无 Flag 的运行时对象生成
限制:
- 需 TypeScript 4.9+ 版本支持
- 无法在运行时访问 Enum(如 Flag.A 会报错)
- 设计意图:帮助旧项目逐步迁移,而非推荐使用 Enum。
4. 替代方案的正确实现
1. 联合类型 + as const(零开销方案)
将一个对象断言(Type Assertion)为一个 const,此时这个对象的类型就是对象字面量类型。
const HTTP_STATUS = {
OK: 200,
NotFound: 404,
} as const;
// 类型推导为 { readonly OK: 200; readonly NotFound: 404; }
type HttpStatus = typeof HTTP_STATUS[keyof typeof HTTP_STATUS]; // 200 | 404
// 编译为
var HTTP_STATUS = {
OK: 200,
NotFound: 404,
};
优势:运行时仅为普通对象,类型精确到字面量(200 而非 number)。
2. 模板字面量类型(动态枚举)
enum LogLevelEnum {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
}
type LogLevelString = `${LogLevelEnum}`;
function setLevel(level: LogLevelString) { } // 直接约束为字面量
setLevel('info')
setLevel(LogLevelEnum.DEBUG)
编译后:
var LogLevelEnum;
(function (LogLevelEnum) {
LogLevelEnum["DEBUG"] = "debug";
LogLevelEnum["INFO"] = "info";
LogLevelEnum["WARN"] = "warn";
})(LogLevelEnum || (LogLevelEnum = {}));
function setLevel(level) { } // 直接约束为字面量
setLevel('info');
setLevel(LogLevelEnum.DEBUG);
优点:不会产生任何运行时代码,TS 只在编译期检查,更灵活,可以轻松扩展。
3. const enum
const enum Size {
Small = 'S',
Medium = 'M',
Large = 'L'
} // 无任何代码生成!完全被擦除
console.log(Size.Small); // 编译为 console.log("S")
function getSize(size: Size) {
return size;
}
getSize(Size.Small);
使用建议:仅当需要内联枚举值且不需要运行时对象时使用。
注意点 📢:
1、需要 isolatedModules 配置,影响使用
const enum 只能在 非 isolatedModules: true 的情况下正常使用。当启用 isolatedModules(比如在使用 Babel 或 ES 模块编译时),TS 不允许 const enum,否则会报错:
// isolatedModules: true 时,报错:
const enum Status {
Success = "SUCCESS",
Fail = "FAIL"
}
console.log(Status.Success);
Const enums are not allowed when the '--isolatedModules' flag is provided.
原因:const enum 需要在 TypeScript 编译时展开为字面量,而 isolatedModules 规定了每个文件必须独立编译,TS 编译器无法跨文件进行内联展开。
2、可能导致编译错误,但不会提示
const enum 是在编译时展开的,所以如果从另一个模块导入 const enum,但没有正确导出,它不会报错,甚至不会有警告,但是运行时会崩溃。
错误示例
// a.ts
const enum Status {
Success = "SUCCESS",
Fail = "FAIL"
}
export { Status };
// b.ts
import { Status } from "./a";
console.log(Status.Success); // ❌ 运行时报错
原因:const enum 在 使用时会被展开,但 Status 并没有在 a.ts 编译后留下任何定义,导致 b.ts 运行时报错。
3、const enum 在 JavaScript 运行时完全消失,不利于调试
4、不能与 --preserveConstEnums 选项一起使用
TS 提供了 --preserveConstEnums 选项,允许普通 enum 仍然保留在编译后的 JS 代码中,但这个选项 对 const enum 无效,也就是说 const enum 一定会被擦除,不可恢复。
5、const enum 不能在动态访问中使用
普通 enum 允许使用 动态 key 访问,但 const enum 由于在编译后被擦除,不能这样做:
enum Status {
Success = "SUCCESS",
Fail = "FAIL"
}
const key = "Success";
console.log(Status[key]); // ✅ 可行
但如果使用 const enum:
const enum Status {
Success = "SUCCESS",
Fail = "FAIL"
}
const key = "Success";
console.log(Status[key]); // 编译报错,无法动态访问