Skip to content

为什么我不喜欢枚举

Copyright Statement

Translation and Republication Notice:

This translation is provided for educational and informational purposes only. All intellectual property rights, including copyright, remain with the original author and/or publisher. This translation maintains the integrity of the original content while making it accessible to chinese readers.

Modifications Disclosure:

  • This is a complete and faithful translation of the original content with no substantive modifications.
  • This translation includes minor adaptations to improve clarity for chinese readers while preserving all essential information and viewpoints.
  • Sections marked with [†] contain supplementary explanations added by the translator to provide cultural or technical context.

Rights Reservation:

If you are the copyright holder and believe this translation exceeds fair use guidelines, please contact us at email. We are committed to respecting intellectual property rights and will promptly address any legitimate concerns.

背景概述

首先需要明确一点,我喜欢枚举(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 标记

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

结论

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

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

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

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

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

贡献者

页面历史

Discuss

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