TypeScript:为什么应该停止使用 Enum?

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 主要作用:

  1. 消除运行时代码污染:强制只使用“纯类型”,减少 JS 代码大小,提高运行时性能。
  2. 确保 TS 代码更接近 JS 语法,不依赖 TS 特有的扩展语法(如 enum)。
  3. 让 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]); // 编译报错,无法动态访问
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值