Avoid using TypeScript's Enums, use 'as const' instead
Last updated:
TypeScript 的 Enums 是复杂的和不协调的,这种混乱时常让我感到烧脑,直至某天,我停了下来,然后开始怀疑「Enums 是不是不良特性」。
是的,Enums 是不良特性,因为 Enums 做的事,别的特性也能做,但 Enums 有许多额外的负担。
# Enums 的混乱
Enums 是复杂的和不协调的,为什么?因为 Enums 的用法真的很多,而且 Numeric Enums 和 String Enums 的行为还有很多差别。
TypeScript 将 Enums 分类为 Numeric Enums、String Enums、Heterogeneous Enums,其中的 Heterogeneous Enums 是前两者的混合物。
第一,Numeric Enums 有「自增」和「双向映射」,但 String Enums 没有:
/**
* { "0": "Admin", "1": "Sales", "Admin": 0, "Sales": 1 }
*/
enum NumericRole {
Admin, // 0
Sales, // 1
}
/**
* { "Admin": "admin", "Sales": "sales" }
*/
enum StringRole {
Admin = 'admin', // "admin"
Sales = 'sales', // "sales"
}
第二,在 Numeric Enums 中,互为逆映射的 2 个键值对是不对称,比如 "Admin": 0 的逆映射是 "0": "Admin" 而不是 0: "Admin",这是因为 JavaScript Object 的键只接受 string 或 symbol。
第三,Heterogeneous Enums 也有自增和双向映射,但只有一部份:
/**
* { "2": "Admin", "3": "Sales", Admin: 2, Sales: 3, Client: "Client" }
*/
enum HeterogeneousRole {
Admin = 2, // 2
Sales, // 3
Client = 'Client', // "Client"
}
第四,Enums 既是值,也是类型。如果你想取 NumericRole 的键的联合类型,那么 NumericRole 要作为值还是类型?取到的联合类型会有 2 个成员("Admin" | "Sales")还是 4 个成员("0" | "1" | "Admin" | "Sales")?
type K = keyof typeof NumericRole; // "Admin" | "Sales"
第五,如果用 Enums 来作为类型,那么类型会严格的过分,而且 Numeric Enums 和 String Enums 的严格程度还不一样:
// 🙋🏻 Correct
0 satisfies NumericRole.Admin;
1 satisfies NumericRole.Sales;
// 🙅🏻 Type Error
'admin' satisfies StringRole.Admin;
'sales' satisfies StringRole.Sales;
enum SameNumericRole {
Admin,
Sales,
}
enum SameStringRole {
Admin = 'admin',
Sales = 'sales',
}
// 🙅🏻 Type Error
SameStringRole.Sales satisfies StringRole.Sales;
SameNumericRole.Admin satisfies NumericRole.Admin;
最后,还有 const enums,一个可被擦除的 Enums,库开发中的老陷阱。
# Enums 的上位替代
Enums 的上位替代就是对象常量,对象常量是指那些使用了类型断言 as const 的对象,就像下面这样:
const role = { admin: 0, sales: 1 } as const;
为什么是对象常量?因为对象常量有 Enums 的核心功能——存储常量的字典,而且还没有 Enums 的混乱,虽然少了很多特性,比如自增、双向映射、直接作为类型等,但这些特性本就是混乱之源。
enum Role {
Admin,
Sales,
}
const role = { admin: 0, sales: 1 } as const;
fetch(`/data?role=${role.admin}`); // 🙋🏻
fetch(`/data?role=${Role.Admin}`); // 🙅🏻
用对象常量,而不是 Enums,这已经是不言自明的常识了。
# 禁用 Enums:erasableSyntaxOnly
TypeScript 5.8 推出了一个新的规则 erasableSyntaxOnly,其字面意思就是「仅启用可擦除的语法」,它的作用是禁用掉那些会对值空间产生影响的特性,即 Enums、Namespaces、Classes。
{
"compilerOptions": {
"erasableSyntaxOnly": true
}
}
这代表了 TypeScript 官方的计划——在未来废除它们。