enum、const enum 和 as const,应该如何列举资料于 TypeScript 当中?
寻找列举资料的方法
近期重写的专案中有许多状态需要管理,会需要统一管理资料于专案中,为了避免写死代码并且让接手的人都能轻易地了解资料型态,这里记录一些过程中的发现。举例来说目前有个警告程度资料:
如果我在专案不同地方需要使用到或会接受到这笔资料,要怎么确保「依靠单一资料来源」去提示不同地方会接收到这笔资料呢?
使用列举 enum
由于是 TypeScript 专案,我第一时间想到的是使用列举(Enums)来管理资料,枚举是特殊的「非型别层级」的 TS,用于表示一组常数(不可变的数值)。它有些怪怪的魔法在里面并不是所有人都喜欢,比如说一个简单的 enum:
实际编译出来会是以下这坨东东:
也就是像是这样对象的效果:
以上是 Numberic Enums 所以通常会使用 String Enums :
也就是说 Enums 型别是独特的,就算有另一个一样型别定义的 LogLevel 还是会被视为不同型别,这样的语法让通常是 Structural Type 特性的 TS 强化为 Nominal Type 系统的特性,背后 TS 使用了一些魔法抽象但实际上还是在包装一个 JS 对象。
- 不是很喜欢 TS 的一些难以预知的魔法转换(如果不定义值会建立成 Numeric Enums,通常是不乐见的)
- 型别系统的特性稍微不同,不过我想不是大问题
- TypeScript Team 讨论如果能现在重来,大概不会添加这个功能
使用 const enum
这种做法会让 TypeScript 只处理枚举的值在类型上,也就是说并不会有任何 JS 在编译后产生,这听起来很干净且直觉!TS 会直接在编译时将使用到 const enum
的地方替换成对应的值。不过在 TS 官方文件基本上完全不推荐使用 const enum
,如果使用在共享代码库中没有办法控制编译器可能会造成问题。
使用 as const
结果绕了一大圈原来最好的方法就在脚下??介绍老 JS 对象 POJO (Plain Old JavaScript Object),其实就是一个 JS 对象使用 as const
告知 TS 这个对象是完全不可变的,像是 Object.freeze(),不过是深层次并且不存在于运行时,真正意义上不变的值。
如果不熟悉 JS 的特性的话可能会认为 JS 的 const
就意味着声明内容不可变的变量,但实际上这里的不可变意思是指变量不可再被指派新内存地址,也是因为这样的特性 TS 并没有办法确定 const
声明的变量是否为不可变的值。
但当我们在 TS 中使用 as const
就真的让 TS 知道是不可变的,于是我们能拿到更为明确的类型:
好耶!也就是说我们可以用 keyof
和 typeof
来取得这个对象的类型,并且运用在任何地方:
甚至制作一个工具类型来帮助我们做转换:
抉择
讨论不要使用 Enum 的论点可以总结为:
- 在编译后成果有点怪异,这些怪异的点会需要特别观察编译结果或阅读文档才能了解
- Enum 在使用上会更加死板(必须传入 Enum 作为值、相比于
as const
只需要传入对应的值即可)
不过我认为 Enum 也有它的优点:
- Enum 名称与用途都非常明确
- 非常死板,所有值只能够过 Enum 输入,确保数据的正确性
完全使用 as const
来达成枚举数据管理,因为它更加直觉没有什么认知负担,并且更加灵活。
延伸阅读
- Enums - TypeScript
- Enums considered harmful - Matt Pocock
- The TRUTH About TypeScript Enums - James Q Quick
- as const: the most underrated TypeScript feature - Matt Pocock
- TypeScript Enums are TERRIBLE. Here’s Why. - Michigan TypeScript