大数跨境

改变一切的代码审查

改变一切的代码审查 索引目录
2025-10-21
2
导读:关注「索引目录」公众号,获取更多干货。三个月前,我提交了一个我认为非常合理的拉取请求。

关注「索引目录」公众号,获取更多干货。

三个月前,我提交了一个我认为非常合理的拉取请求。我创建了一个新的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 */

常量枚举在编译时内联。它们不会创建运行时对象。

然而:

  1. 它们不适用于isolatedModules(Babel、esbuild、SWC 所需)
  2. 他们被弃用,取而代之的是preserveConstEnums
  3. 它们比仅仅使用对象更复杂

我的建议:即使是 const 枚举,也只使用对象。越简单越好。


第 8 部分:现实世界的影响

当我们将代码库从枚举迁移到 const 对象时,发生了以下情况:

迁移之前

  • 代码库中的枚举:
     47
  • 捆绑包大小:
     2.4 MB(最小化)
  • 捆绑包中与枚举相关的代码:
     ~14 KB

迁移后

  • 代码库中的枚举:
     0
  • 捆绑包大小:
     2.388 MB(最小化)
  • 节省:
     12 KB

“只有12KB?”

是的,但是:

  1. 12KB 的数据我们不需要发送、解析或执行
  2. 类型安全性得到改善(我们在迁移过程中发现了 3 个错误)
  3. 代码变得更易读(它只是 JavaScript)
  4. 新开发人员上手更快(TypeScript 怪癖更少)

开发者体验改进

  1. 更快的编译:
     TypeScript 不需要生成枚举代码
  2. 更好的 IDE 性能:
    需要跟踪的运行时构造更少
  3. 更容易调试:
    控制台日志显示实际值,而不是枚举引用
  4. 更简单的思维模型:
    少记住一个 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>

关注「索引目录」公众号,获取更多干货。


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读544
粉丝0
内容444