为什么我不喜欢枚举
版权声明
翻译与转载须知:
本译文仅供教育和信息交流之用。所有知识产权(包括版权)均归原作者和/或出版商所有。本译文在保持原文内容完整性的同时,旨在使其更易于中文读者理解。
修改声明:
- 本文为原文的完整忠实译本,未进行任何实质性修改。
- 本译文包含为提升中文读者清晰度而做的微小调整,同时保留了所有基本信息和观点。
- 标有 [†] 的部分包含译者为提供文化或技术背景而添加的补充说明。
权利保留:
如果您是版权所有者,并认为本译文超出了合理使用范围,请通过 邮箱 与我们联系。我们致力于尊重知识产权,并将及时处理任何正当关切。
背景概述
首先需要明确一点,我喜欢枚举(enums
)这个概念。我非常希望 JavaScript
能原生支持枚举。
然而,TypeScript
对枚举的实现方式存在一些特殊性,这使我不建议在实际项目中使用它们。
枚举类型的分类
在 TypeScript
中,可以声明三种不同类型的枚举值:数值型(Numeric
)、字符串型(String
)和推断型(Inferred
):
// 数值型枚举
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
)?
enum PackStatus {
Draft = 0,
Approved = 1,
Shipped = 2
}
答案是 6
个。这是因为 TypeScript
为数值型枚举生成了值到键(value-to-key
)和键到值(key-to-value
)的双向映射。
而对于字符串型枚举,它不会这样处理,因此只有预期的 3
个键:
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
强制要求传入枚举值:
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'.
但对于数值型枚举,在期望枚举的地方可以直接传入原始数值。这一点令人困惑:
enum PackStatus {
Draft = 0,
Approved = 1,
Shipped = 2
}
const logStatus = (status: PackStatus) => {
console.log(status);
};
logStatus(PackStatus.Draft);
// 没有错误。这是为什么?
logStatus(0);
更令人惊讶的是,你甚至可以在同一个枚举中混合使用数值型和字符串型值。
请不要这样做:
enum PackStatus {
Draft = 'Draft',
Approved = 2,
Shipped // 推断为 3
}
标称类型(Nominal Typing)问题
我对枚举的最后一个不满是较为主观的。
我希望我的 TypeScript
代码仅仅是添加了类型的 JavaScript
。而枚举似乎打破了这个规则。
例如,即使值完全相同,你也不能在需要一个枚举的地方使用另一个枚举:
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'.
即使第二个枚举只是对第一个枚举的引用,情况依然如此:
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
标记。
据我所知,由于枚举的实现方式,许多这些问题可能无法解决。
结论
如果在现有代码库中看到枚举,我不太可能去移除它。如果它使用的是推断值,我可能会显式地添加这些值。
但我不会在全新的代码库中添加枚举。
如果你确实需要使用枚举,我强烈建议只使用字符串型枚举。
它们更加明确,行为更接近真正的枚举,并且看起来更像它们转译后的代码。
如果你想了解我使用什么来代替枚举,可以查看我书中的 相关章节。