WARNING: 该文章的观点可能已经过时,仅供参考。
概述
Javascript 作为一种脚本语言,并没有强类型语言的内置枚举。
Typescript 作为 Javascript 的超集,提供了枚举。
Typescript 中的枚举和 Class 一样,既是类型,又可以作为值使用。
Typescript 提供了两种枚举: 常规枚举
enum
和常量枚举 const enum
。enum
会被编译成普通 Javascript 对象,在运行时引用。const enum
会在编译时直接内联替换 value,不产生运行时消耗。枚举的值可以为
string
或 number
。对于
enum
,在枚举值为 number
的情况下,会同时创建 value: key
对,以便可以通过 number
来访问枚举的 key
。以上就是 Typescript 枚举的特性。
但是,Typescript 中的枚举并不是完美无缺的。由于历史遗留和一些其他问题,枚举的使用受到了很大的限制。
枚举的定义
维基百科中是这样定义的:
枚举是组织收集有关联变量的一种方式。
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象(实例)的计数。
不考虑静态语言和动态语言的差异,枚举主要的目的就是简化如下代码:
let PREPARE = 0; let RUNNING = 1; let DONE = 2; // 使用枚举 enum Status { PREPARE, RUNNING, DONE }
Typescript 中的枚举的问题
ESM 不兼容
由于早期设计思路问题,早期 Typescript 是作为可以编译成 Javascript 的强类型语言来开发,并提供了
namespace
和 enum
等不需要 import
和 export
的特性。当
enum
作为类型使用的时候没有问题,但是作为值的时候,一个不需要 import
就能使用的值,很明显违背了当下流行的 ES Module(ESM)。幸好,Typescript 提供了
--isolatedModules
配置。当该配置启用时,所有依赖扫盘的,不 ESM 的特性都会被禁用。始终应该在
--isolatedModules
下使用枚举,而不使用遗留枚举。否则在迁移到 vite 之类的 ESM bundle 工具时会出现问题。
编译器表现复杂
tsc
对于
tsc
,在启用和不启用 --isolatedModules
下, const enum
的编译表现不同。启用后
const enum
会按照 enum
来编译。源代码:
isolatedModules: true
isolatedModules: false
而
preserveConstEnums
Options 和 isolatedModules
是互斥的,无法同时设置。babel
对于 babel,早期根本不支持
const enum
编译。 const enum
和 namespace
是 @babel/preset-typescript
中规定的不支持语法。在 babel v7.15.0+ 中,已经支持了
const enum
,但是还是有一些特殊处理。默认情况下
enum
和 const enum
都按 enum
编译,和 Typescript 处理 --isolatedModules
的行为类似。// Input const enum Animals { Fish } console.log(Animals.Fish); // Default output var Animals; (function (Animals) { Animals[Animals["Fish"] = 0] = "Fish"; })(Animals || (Animals = {})); console.log(Animals.Fish); // `optimizeConstEnums` output console.log(0);
在 babel v7.15.0+ 中,提供了新的
optimizeConstEnums
配置,默认为 false。开启后,将会尽量恢复常量枚举的行为。
当 const enum
没有 export
时,此时表现和 const enum
的定义相同,即直接编译替换,不产生运行时。
当 const enum
有 export
时,将其作为普通对象导出,产生运行时,但是不产生 enum
的 key-value 颠倒。
可以看出 babel 的
optimizeConstEnums: true
才更贴近 const enum
的语义。当下 tsc 的处理其实并不是很完美。最佳实践
几种变量组织方法对比
字面量联合
type Status = "PREPARE"| "RUNNING"| "DONE"; type Status = 0 | 1 | 2;
可以看到使用
string
字面量联合确实可以更清晰,减少 import enum
的心智负担。这也是大多数组件库提供 props 的方法。但是
number
字面量联合表达不出语义,并不是很好的实践。枚举
enum Status { PREPARE, RUNNING, DONE } const enum Status { PREPARE, RUNNING, DONE }
枚举在 value 是
number
时很有意义,可以减少压缩后的代码。const enum
可以实现 0 运行时,因为会在编译时做替换。但是由于需要 import 使用,不适合常规组件的 props 交互场景。在 value 为 string 的情况下,枚举变得意义不大。因为既不能
keyof
获取联合类型,又不能快捷生成 value-key 对。Object as const
let Status = { PREPARE: "PREPARE", RUNNING: "RUNNING", DONE: "DONE" } as const
这种方式可以通过
keyof
和 ValueOf
快速获取字面量联合类型,是 value 为 string
时比较推荐的方法。推荐实践
始终应该在
--isolatedModules
下使用枚举,而不使用遗留枚举。使用枚举将字符串常量映射成 number 是有意义的,可以减少压缩后的代码。
const enum
可以实现 0 运行时,因为会在编译时做替换。对于 value 为 string 的情况,不应该使用任何枚举。
keyof
订正: 可以使用keyof typeof
来获取enum
的 key 联合类型,但是不能使用ValueOf
获取 value 的联合类型。结论保持不变,还是不推荐在 value 为 string 的时候使用enum
,Object as const
是更好的选择。
应该使用普通对象加
as const
来替代,如不需要运行时,可以使用 string 字面量联合类型和 string 字面量来实现。
在 value 为 number 的情况下,应该始终使用枚举。
如果需要通过 index 互访问,使用常规枚举。如果不需要,使用常量枚举。
但是都不能违背本来的含义: 枚举是组织收集有关联变量的一种方式。
感谢阅读!
Loading Comments...