关于 TypeScript 枚举的一点思考

关于 TypeScript 枚举的一点思考

Tags
TypeScript
Published
Sep 8, 2022
WARNING: 该文章的观点可能已经过时,仅供参考。

概述

Javascript 作为一种脚本语言,并没有强类型语言的内置枚举。
Typescript 作为 Javascript 的超集,提供了枚举。
Typescript 中的枚举和 Class 一样,既是类型,又可以作为值使用。
Typescript 提供了两种枚举: 常规枚举 enum 和常量枚举 const enum
 
enum 会被编译成普通 Javascript 对象,在运行时引用。
const enum 会在编译时直接内联替换 value,不产生运行时消耗。
 
枚举的值可以为 stringnumber
对于 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 的强类型语言来开发,并提供了 namespaceenum 等不需要 importexport 的特性。
enum 作为类型使用的时候没有问题,但是作为值的时候,一个不需要 import 就能使用的值,很明显违背了当下流行的 ES Module(ESM)。
幸好,Typescript 提供了--isolatedModules 配置。当该配置启用时,所有依赖扫盘的,不 ESM 的特性都会被禁用。
始终应该在 --isolatedModules 下使用枚举,而不使用遗留枚举。
否则在迁移到 vite 之类的 ESM bundle 工具时会出现问题。

编译器表现复杂

tsc

对于 tsc ,在启用和不启用 --isolatedModules 下, const enum 的编译表现不同。
 
启用后 const enum 会按照 enum 来编译。
 
源代码:
notion image
isolatedModules: true
notion image
isolatedModules: false
notion image
 
preserveConstEnums Options 和 isolatedModules 是互斥的,无法同时设置。
notion image

babel

对于 babel,早期根本不支持 const enum 编译。 const enumnamespace@babel/preset-typescript 中规定的不支持语法。
在 babel v7.15.0+ 中,已经支持了 const enum ,但是还是有一些特殊处理。
 
默认情况下 enumconst 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 的定义相同,即直接编译替换,不产生运行时。
notion image
notion image
const enumexport 时,将其作为普通对象导出,产生运行时,但是不产生 enum 的 key-value 颠倒。
notion image
notion image
 
可以看出 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
这种方式可以通过 keyofValueOf 快速获取字面量联合类型,是 value 为 string 时比较推荐的方法。
 

推荐实践

始终应该在 --isolatedModules 下使用枚举,而不使用遗留枚举。
 
使用枚举将字符串常量映射成 number 是有意义的,可以减少压缩后的代码。 const enum 可以实现 0 运行时,因为会在编译时做替换。
 
对于 value 为 string 的情况,不应该使用任何枚举。
因为既不能 keyof 获取联合类型,又不能快捷生成 value-key 对。
订正: 可以使用 keyof typeof 来获取 enum 的 key 联合类型,但是不能使用 ValueOf 获取 value 的联合类型。结论保持不变,还是不推荐在 value 为 string 的时候使用 enumObject as const 是更好的选择。
应该使用普通对象加 as const 来替代,如不需要运行时,可以使用 string 字面量联合类型和 string 字面量来实现。
在 value 为 number 的情况下,应该始终使用枚举。
如果需要通过 index 互访问,使用常规枚举。如果不需要,使用常量枚举。
但是都不能违背本来的含义: 枚举是组织收集有关联变量的一种方式。
 
 
感谢阅读!

Loading Comments...