enum、const enum 和 as const,应该如何列举资料于 TypeScript 当中?
寻找列举资料的方法
近期重写的专案中有许多状态需要管理,会需要统一管理资料于专案中,为了避免写死代码并且让接手的人都能轻易地了解资料型态,这里记录一些过程中的发现。举例来说目前有个警告程度资料:
const LogLevel = { DEBUG: 'DEBUG', WARNING: 'WARNING', ERROR: 'ERROR',};
如果我在专案不同地方需要使用到或会接受到这笔资料,要怎么确保「依靠单一资料来源」去提示不同地方会接收到这笔资料呢?
// 我只希望这个函式接收到 LogLevel 这笔资料,而不是其他任何型态function doSomeThing(level: unknown) { // Do Something}
使用列举 enum
由于是 TypeScript 专案,我第一时间想到的是使用列举(Enums)来管理资料,枚举是特殊的「非型别层级」的 TS,用于表示一组常数(不可变的数值)。它有些怪怪的魔法在里面并不是所有人都喜欢,比如说一个简单的 enum:
enum LogLevel { DEBUG, WARNING, ERROR,}
实际编译出来会是以下这坨东东:
var LogLevel;(function (LogLevel) { LogLevel[(LogLevel['DEBUG'] = 0)] = 'DEBUG'; LogLevel[(LogLevel['WARNING'] = 1)] = 'WARNING'; LogLevel[(LogLevel['ERROR'] = 2)] = 'ERROR';})(LogLevel || (LogLevel = {}));
也就是像是这样对象的效果:
const LogLevel = { DEBUG: 0 0: 'DEBUG', WARNING: 1, 1: 'WARNING', ERROR: 2, 2: 'ERROR',};
以上是 Numberic Enums 所以通常会使用 String Enums :
enum LogLevel { DEBUG = 'DEBUG', WARNING = 'WARNING', ERROR = 'ERROR',}
function doSomeThing(level: LogLevel) { // Do Something}
doSomeThing(LogLevel.DEBUG);
也就是说 Enums 型别是独特的,就算有另一个一样型别定义的 LogLevel 还是会被视为不同型别,这样的语法让通常是 Structural Type 特性的 TS 强化为 Nominal Type 系统的特性,背后 TS 使用了一些魔法抽象但实际上还是在包装一个 JS 对象。
- 不是很喜欢 TS 的一些难以预知的魔法转换(如果不定义值会建立成 Numeric Enums,通常是不乐见的)
- 型别系统的特性稍微不同,不过我想不是大问题
- TypeScript Team 讨论如果能现在重来,大概不会添加这个功能
使用 const enum
这种做法会让 TypeScript 只处理枚举的值在类型上,也就是说并不会有任何 JS 在编译后产生,这听起来很干净且直觉!TS 会直接在编译时将使用到 const enum
的地方替换成对应的值。不过在 TS 官方文件基本上完全不推荐使用 const enum
,如果使用在共享代码库中没有办法控制编译器可能会造成问题。
const enum LogLevel { DEBUG, WARNING, ERROR,}
使用 as const
结果绕了一大圈原来最好的方法就在脚下??介绍老 JS 对象 POJO (Plain Old JavaScript Object),其实就是一个 JS 对象使用 as const
告知 TS 这个对象是完全不可变的,像是 Object.freeze(),不过是深层次并且不存在于运行时,真正意义上不变的值。
如果不熟悉 JS 的特性的话可能会认为 JS 的 const
就意味着声明内容不可变的变量,但实际上这里的不可变意思是指变量不可再被指派新内存地址,也是因为这样的特性 TS 并没有办法确定 const
声明的变量是否为不可变的值。
const logLevel = { DEBUG: 'DEBUG', WARNING: 'WARNING', ERROR: 'ERROR',};
logLevel.DEBUG = 'WHATEVER'; // 可以被重新指派内容
// 类型:// const logLevel: {// DEBUG: string;// WARNING: string;// ERROR: string;// }
但当我们在 TS 中使用 as const
就真的让 TS 知道是不可变的,于是我们能拿到更为明确的类型:
const logLevel = { DEBUG: 'DEBUG', WARNING: 'WARNING', ERROR: 'ERROR',} as const;
// 类型// const logLevel: {// readonly DEBUG: "DEBUG";// readonly WARNING: "WARNING";// readonly ERROR: "ERROR";// }
好耶!也就是说我们可以用 keyof
和 typeof
来取得这个对象的类型,并且运用在任何地方:
type LogLevel = (typeof logLevel)[keyof typeof logLevel];
甚至制作一个工具类型来帮助我们做转换:
type ObjectValues<T> = T[keyof T];
const logLevel = { DEBUG: 'DEBUG', WARNING: 'WARNING', ERROR: 'ERROR',} as const;
type LogLevel = ObjectValues<typeof logLevel>;
function doSomeThing(level: LogLevel) { // Do Something}
doSomeThing('DEBUG');
抉择
讨论不要使用 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