稀土掘金技术社区 03月20日
TypeScript 官方宣布弃用 Enum?Enum 何罪之有?
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文探讨了TypeScript中Enum的使用争议以及官方推出erasableSyntaxOnly配置的原因,该配置旨在配合Node.js对TS文件的支持,允许直接执行可擦除语法的TS代码。文章深入分析了Enum的三大问题:默认值带来的类型安全隐患、不支持枚举值字面量以及增加运行时开销。同时,文章还介绍了五种替代方案,包括const enum、模板字面量类型、联合类型、类型字面量 + as const以及Class类静态属性自定义实现,以便开发者根据实际需求选择合适的方案。

⚠️ TypeScript的`erasableSyntaxOnly`配置旨在配合Node.js对TS文件的支持,允许直接执行可擦除语法的TS代码,而非放弃Enum,反映了官方对减少运行时开销和优化编译输出的关注。

⚖️ Enum存在三大问题:一是枚举默认值从0开始,可能引发类型安全问题;二是不支持枚举值字面量,限制了使用的灵活性;三是编译后会生成额外的JavaScript双向映射数据,增加运行时开销。

💡 Enum的替代方案有多种,包括const enum、模板字面量类型、联合类型、类型字面量 + as const以及Class类静态属性自定义实现。其中,类型字面量 + as const是一种比较常用的方案,它既可以传入枚举值,又可以传入枚举类型。

💎 Class 类静态属性自定义实现:通过TS面向对象的特性,自定义实现一个枚举类,类似于后端定义枚举的方式,具有很好的扩展性,自定义程度更高。

原创 MurphyChen 2025-03-18 08:31 重庆

点击关注公众号,“技术干货” 及时达!


点击关注公众号,“技术干货” 及时达!

1. 官方真的不推荐 Enum 了吗?

1.1 事情的起因

起因是看到 科技爱好者周刊(第 340 期) 里面推荐了一篇文章,说是官方不再推荐使用 enum 语法,原文链接在 An ode to TypeScript enums,大致是说 TS 新出了一个 --erasableSyntaxOnly 配置,只允许使用可擦除语法,但是 enum 不可擦除,因此推断官方已不再推荐使用 enum 了。官方并没有直接表达不推荐使用,那官方为什么要出这个配置项呢?


1.2 什么是可擦除语法

就在上周,TypeScript 发布了 5.8 版本,其中有一个改动是「添加了 --erasableSyntaxOnly 配置选项,开启后仅允许使用可擦除语法,否则会报错」enum 就是一个不可擦除语法,开启 erasableSyntaxOnly 配置后,使用 enum 会报错。

例如,如果在 tsconfig 文件中配置 "erasableSyntaxOnly": true(只允许可擦除语法),此时使用不可擦除语法,将会得到报错:

「可擦除语法就是可以直接去掉的、仅在编译时存在、不会生成额外运行时代码的语法,例如 typeinterface。不可擦除语法就是不能直接去掉的、需要编译为JS且会生成额外运行时代码的语法,例如 enumnamesapce(with runtime code)。」 具体举例如下:

可擦除语法,不生成额外运行时代码,比如 typelet n: numberinterfaceas number 等:

不可擦除语法,生成额外运行时代码,比如 enumnamespace(具有运行时行为的)、类属性参数构造语法糖(Class Parameter properties)等:

// 枚举类型
enum METHOD {
  ADD = 'add'
}
// 类属性参数构造
class A {
  constructor(public x: number) {}
}
let a: number = 1
console.log(a)

需要注意,具有运行时行为的 namespace 才属于不可擦除语法。

// 不可擦除,具有运行时逻辑
namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }
}
// 可擦除,仅用于类型声明,不包含任何运行时逻辑
namespace Shapes {
  export interface Rectangle {
    width: number;
    height: number;
  }
}

1.3 TS 官方为什么要出 erasableSyntaxOnly?

官方既然没有直接表达不推荐 enum,那为什么要出 erasableSyntaxOnly 配置来排除 enum 呢?

我找到了 TS 官方文档(The --erasableSyntaxOnly Option)说明:


大致意思是说之前 Node 新版本中支持了执行 TS 代码的能力,可以直接运行包含可擦除语法的 TypeScript 文件。Node 将用空格替换 TypeScript 语法,并且不执行类型检查。总结下来就是:

在 Node 22 版本:

在 Node 23.6.0 版本:

综上所述,TS 官方为了配合 Node.js 这次改动(即默认允许直接执行不可擦除语法的 TS 代码),才添加了一个配置项 erasableSyntaxOnly,只允许可擦除语法。

2. Enum 的三大罪行

自 Enum 从诞生以来,它一直是前端界最具争议的特性之一,许多前端开发者乃至不少大佬都对其颇有微词,纷纷发起了 「DO NOT USE TypeScript Enum」 的吐槽。那么enum 真的有那么难用吗?我认为是的,这玩意坑还挺多的,甲级战犯 Enum,出列!

2.1 枚举默认值

enum 默认的枚举值从 0 开始,这还不是最关键的,你传入了默认枚举值时,居然是合法的,这无形之中带来了类型安全问题。

enum METHOD {
    ADD
}
function doAction(method: METHOD{
  // some code
}
doAction(METHOD.ADD) // ✅ 可以
doAction(0// ✅ 可以

2.2 不支持枚举值字面量

还有一种场景,我要求既可以传入枚举类型,又要求传入枚举值字面量,如下所示,但是他又不合法了?(有人说你定义传枚举类型就要传相应的枚举,这没问题,但是上面提到的问题又是怎么回事呢?这何尝不是 Enum 的双标?)

enum METHOD {
    ADD = 'add'
}
function doAction(method: METHOD{
  // some code
}
doAction(METHOD.ADD) // ✅ 可以
doAction('add'// ❌ 不行 

2.3 增加运行时开销

TypeScript 的 enum 在编译后会生成额外的 JavaScript 双向映射数据,这会增加运行时的开销。


3. Enum 的替代方案

众所周知,TS 一大特性是类型变换,我们可以通过类型操作组合不同类型来达到目标类型,又称为类型体操。下面的四种解决方案,可以根据实际需求来选择。

3.1 const enum

const enum 是解决产生额外生成的代码和额外的间接成本有效且快捷的方法,但不推荐使用。

const enum 由于编译时内联带来了性能优化,但在 .d.ts 文件、isolatedModules 兼容性、版本不匹配及运行时缺少 .js 文件等场景下存在隐藏陷阱,可能导致难以发现的 bug。详见官方说明:const-enum-pitfalls

const enum METHOD {
  ADD = 'add',
  DELETE = 'delete',
  UPDATE = 'update',
  QUERY = 'query',
}
function doAction(method: METHOD{
    // some code
}
doAction(METHOD.ADD) // ✅ 可行
doAction('delete'// ❌ 不行

const enum 解析后的代码中引用 enum 的地方将直接被替换为对应的枚举值:


3.2 模板字面量类型

将枚举类型包装为模板字面量类型(Template Literal Types),从而既支持枚举类型,又支持枚举值字面量,但是没有解决运行时开销问题。

enum METHOD {
  ADD = 'add',
  DELETE = 'delete',
  UPDATE = 'update',
  QUERY = 'query',
}
type METHOD_STRING = `${METHOD}`
function doAction(method: METHOD_STRING{
    // some code
}
doAction(METHOD.ADD) // ✅ 可行
doAction('delete'// ✅ 可行
doAction('remove'// ❌ 不行

3.3 联合类型(Union Types)

使用联合类型,引用时可匹配的值限定为指定的枚举值了,但同时也没有一个地方可以统一维护枚举值,如果一旦枚举值有调整,其他地方都需要改。

type METHOD =
  | 'add'
  /**
   * @deprecated 不再支持删除
   */

  | 'delete'
  | 'update'
  | 'query'
function doAction(method: METHOD{
    // some code
}
doAction('delete'// ✅ 可行,没有 TSDoc 提示
doAction('remove'// ❌ 不行

3.4 类型字面量 + as const(推荐)

类型字面量就是一个对象,将一个对象断言(Type Assertion)为一个 const,此时这个对象的类型就是对象字面量类型,然后通过类型变换,达到即可以传入枚举值,又可以传入枚举类型的目的。

const METHOD = {
  ADD:'add',
  /**
  * @deprecated 不再支持删除
  */

  DELETE:'delete',
  UPDATE: 'update',
  QUERY: 'query'
as const
type METHOD_TYPE = typeof METHOD[keyof typeof METHOD]
function doAction(method: METHOD_TYPE{
  // some code
}
doAction(METHOD.DELETE) // ✅ 可行,有 TSDoc 提示
doAction('delete'// ✅ 可行
doAction('remove'// ❌ 不行

3.5 Class 类静态属性自定义实现

还有一种方法,参考了 @Hamm 提供的方法,即利用 TS 面向对象的特性,自定义实现一个枚举类,实际上这很类似于后端定义枚举的一般方式。这种方式具有很好的扩展性,自定义程度更高。

1.  定义枚举基类    ```ts    /**     * 枚举基类     */    export default class EnumBase {      /**       * 枚举值       */      private value!: string
/** * 枚举描述 */ private label!: string
/** * 记录枚举 */ private static valueMap: Map<string, EnumBase> = new Map();
/** * 构造函数 * @param value 枚举值 * @param label 枚举描述 */ public constructor(value: string, label: string) { this.value = value this.label = label const cls = this.constructor as typeof EnumBase if (!cls.valueMap.has(value)) { cls.valueMap.set(value, this) } }
/** * 获取枚举值 * @param value * @returns */ public getValue(): string | null { return this.value }
/** * 获取枚举描述 * @param value * @returns */ public getLabel(): string | null { return this.label }
/** * 根据枚举值转换为枚举 * @param this * @param value * @returns */ static convert<E extends EnumBase>(this: new(...args: any[]) => E, value: string): E | null { return (this as any).valueMap.get(value) || null } } ```2. 继承实现具体的枚举(可根据需要扩展) ```ts /** * 审核状态 */ export class ENApproveState extends EnumBase { /** * 未审核 */ static readonly NOTAPPROVED = new ENApproveState('1', '未审核') /** * 已审核 */ static readonly APPROVED = new ENApproveState('2', '已审核') /** * 审核失败 */ static readonly FAILAPPROVE = new ENApproveState('3', '审核失败') /*** * 审核中 */ static readonly APPROVING = new ENApproveState('4', '审核中') } ```3. 使用
```ts test('ENCancelState.NOCANCEL equal 1', () => { expect(ENApproveState.NOTAPPROVED.getValue()).toBe('1') expect(ENApproveState.APPROVING.getValue()).toBe('4') expect(ENApproveState.FAILAPPROVE.getLabel()).toBe('审核失败') expect(ENApproveState.convert('2')).toBe(ENApproveState.APPROVED) expect(ENApproveState.convert('99')).toBe(null) }) ```

4. 总结

TS 官方为了兼容 Node.js 23.6.0 这种可执行 TS 文件特性,出了 erasableSyntaxOnly 配置禁用不可擦除语法,反映了 TypeScript 官方对减少运行时开销和优化编译输出的关注,而不是要放弃 enum

但或许未来就是要朝着这个方向把 enum 优化掉也说不定呢?

5. 参考链接

                     

点击关注公众号,“技术干货” 及时达!


阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

TypeScript Enum erasableSyntaxOnly 类型安全 替代方案
相关文章