Skip to content

为什么我不喜欢枚举

版权声明

翻译与转载须知

本译文仅供教育和信息交流之用。所有知识产权(包括版权)均归原作者和/或出版商所有。本译文在保持原文内容完整性的同时,旨在使其更易于中文读者理解。

修改声明

  • 本文为原文的完整忠实译本,未进行任何实质性修改。
  • 本译文包含为提升中文读者清晰度而做的微小调整,同时保留了所有基本信息和观点。
  • 标有 [†] 的部分包含译者为提供文化或技术背景而添加的补充说明。

权利保留

如果您是版权所有者,并认为本译文超出了合理使用范围,请通过 邮箱 与我们联系。我们致力于尊重知识产权,并将及时处理任何正当关切。

背景概述

首先需要明确一点,我喜欢枚举(enums)这个概念。我非常希望 JavaScript 能原生支持枚举。

然而,TypeScript 对枚举的实现方式存在一些特殊性,这使我不建议在实际项目中使用它们。

枚举类型的分类

TypeScript 中,可以声明三种不同类型的枚举值:数值型(Numeric)、字符串型(String)和推断型(Inferred):

typescript
// 数值型枚举
enum PackStatus {
  Draft = 0,
  Approved = 1,
  Shipped = 2
}

// 字符串型枚举
enum PackStatus2 {
  Draft = 'Draft',
  Approved = 'Approved',
  Shipped = 'Shipped'
}

// 推断型枚举
enum PackStatus3 {
  Draft, // 推断为 0
  Approved, // 推断为 1
  Shipped // 推断为 2
}

数值型枚举 vs 字符串型枚举

数值型枚举和字符串型枚举在几个方面的行为存在显著差异。

首先,思考一下 PackStatus 对象有多少个键(key)?

typescript
enum PackStatus {
  Draft = 0,
  Approved = 1,
  Shipped = 2
}

答案是 6 个。这是因为 TypeScript 为数值型枚举生成了值到键(value-to-key)和键到值(key-to-value)的双向映射。

而对于字符串型枚举,它不会这样处理,因此只有预期的 3 个键:

typescript
enum PackStatus {
  Draft = 0,
  Approved = 1,
  Shipped = 2
}

// [0, "Draft", 1, "Approved", 2, "Shipped"]
console.log(Object.keys(PackStatus));

enum PackStatus2 {
  Draft = 'Draft',
  Approved = 'Approved',
  Shipped = 'Shipped'
}

// ["Draft", "Approved", "Shipped"]
console.log(Object.keys(PackStatus2));

在类型检查方面,它们的行为也有所不同。

在需要字符串型枚举的地方,TypeScript 强制要求传入枚举值:

typescript
enum PackStatus {
  Draft = 'Draft',
  Approved = 'Approved',
  Shipped = 'Shipped'
}

const logStatus = (status: PackStatus) => {
  console.log(status);
};

logStatus(PackStatus.Draft);

// 正确报错!
logStatus('Draft');

Errors

Argument of type '"Draft"' is not assignable to parameter of type 'PackStatus'.

但对于数值型枚举,在期望枚举的地方可以直接传入原始数值。这一点令人困惑:

typescript
enum PackStatus {
  Draft = 0,
  Approved = 1,
  Shipped = 2
}

const logStatus = (status: PackStatus) => {
  console.log(status);
};

logStatus(PackStatus.Draft);

// 没有错误。这是为什么?
logStatus(0);

更令人惊讶的是,你甚至可以在同一个枚举中混合使用数值型和字符串型值。

请不要这样做:

typescript
enum PackStatus {
  Draft = 'Draft',
  Approved = 2,
  Shipped // 推断为 3
}

标称类型(Nominal Typing)问题

我对枚举的最后一个不满是较为主观的。

我希望我的 TypeScript 代码仅仅是添加了类型的 JavaScript。而枚举似乎打破了这个规则。

例如,即使值完全相同,你也不能在需要一个枚举的地方使用另一个枚举:

typescript
enum PackStatus {
  Draft = 'Draft',
  Approved = 'Approved',
  Shipped = 'Shipped'
}

enum PackStatus2 {
  Draft = 'Draft'
}

const logStatus = (status: PackStatus) => {
  console.log(status);
};

// 报错,尽管值相同
logStatus(PackStatus2.Draft);

Errors

Argument of type 'PackStatus2' is not assignable to parameter of type 'PackStatus'.

即使第二个枚举只是对第一个枚举的引用,情况依然如此:

typescript
enum PackStatus2 {
  Draft = PackStatus.Draft
}

const logStatus = (status: PackStatus) => {
  console.log(status);
};

// 报错,尽管值相同
logStatus(PackStatus2.Draft);

Errors

Argument of type 'PackStatus2' is not assignable to parameter of type 'PackStatus'.

目前在 TypeScript 代码库中有 71 个与枚举相关的 bug 标记

据我所知,由于枚举的实现方式,许多这些问题可能无法解决。

结论

如果在现有代码库中看到枚举,我不太可能去移除它。如果它使用的是推断值,我可能会显式地添加这些值。

但我不会在全新的代码库中添加枚举。

如果你确实需要使用枚举,我强烈建议只使用字符串型枚举。

它们更加明确,行为更接近真正的枚举,并且看起来更像它们转译后的代码。

如果你想了解我使用什么来代替枚举,可以查看我书中的 相关章节

根据 CC BY-SA 4.0 许可证发布。 (95ee605)