TypeScript 设计的初衷是 JavaScript + Types,所有 TypeScript 的特性不改变运行时的行为
反过来说,如果在 TS 代码中去掉静态类型,应该得到一份完整有效的 JS 代码
这样的好处在于,我们可以通过 ESbuild 而不是 tsc 完成我们的 TS 代码到 JS 代码的转换
但实际上 TypeScript 中有一个特殊类型破坏了这种构想,它就是 Enum
一、什么是 Enum
在 TypeScript 中可以通过 enum 来定义一组常量,并将这些常量放到同一个对象中管理:
enum Language {
ZH_CN = 'zh_CN',
ZH_HK = 'zh_HK',
ZH_TW = 'zh_TW',
EN_US = 'en_US',
EN_GB = 'en_GB',
}
和 type、interface 类似,enum 可以直接作为静态类型使用
function getLocals(lang: Language) {
return `hello ${lang}`;
}
但在调用这个函数的时候,传入的参数不能是 enum 的值,而应该是 enum 的引用
从这里就会发现 enum 的特性:可以当做对象使用
摘一段官方文档的描述:枚举类型在运行时会被编译为一个对象,包含正向映射(name -> value),如果是数值枚举,还会生成反向映射(value -> name)
其实不只是运行时,普通的枚举类型最终都会编译为对象
// 编译前
enum Enum {
A = 1,
B = 2,
}
// 编译后
var Enum;
(function (Enum) {
// 因为是数值枚举,所以还生成了反向映射
Enum[Enum["A"] = 1] = "A";
Enum[Enum["B"] = 2] = "A";
})(Enum || (Enum = {}));
这时可以考虑使用 const enum 来优化编译结果,它不会编译未使用的枚举项,而且不会生成对象,在编译后只会保留枚举值
// 编译前
enum Enum {
A = 1,
B = 2,
}
const arr = [Enum.A]
// 编译后
var arr = [1 /* A */];
二、Enum 的优缺点
由于 enum 可以当做对象使用,所以在管理常量上非常方便
比如上面的 Language,如果需要将 'zh_CN' 改为 'zh_cn',最终只要调整一下 Language 中 ZH_CN 的值就行,因为在使用的时候都是用的 Language.ZH_CN
除此之外,如果某个数据结构需要用到字符串和数字的双向映射,这时候用 enum 会简单很多,因为数值枚举会生成正向和反向映射
enum Options {
apple = 1,
pear = 2,
lemon = 3,
orange = 4,
}
console.log(Options[1]); // apple
而 enum 的缺点,就是在一开始提到的:违背了 TypeScript = JavaScript + Types 的构想
比如下面的这段 TS 代码:
type DataItem = {
label: string;
value: number | string;
};
function formatLabels(arr: DataItem[]) {
return Array.isArray(arr) ? arr.map((x) => x.label).join(', ') : '';
}
const data: DataItem[] = [
{ label: 'wise', value: 1 },
{ label: 'wrong', value: 2 },
];
formatLabels(data);
如果把 DataItem 删掉,这段代码就变成了完整的 JS 代码
而下面这段使用 enum 的代码
enum Language {
ZH_CN = 'zh_CN',
ZH_HK = 'zh_HK',
ZH_TW = 'zh_TW',
EN_US = 'en_US',
EN_GB = 'en_GB',
}
function getLocals(lang: Language) {
return `hello ${lang}`;
}
getLocals(Language.ZH_CN);
由于 enum 可以当做对象使用,所以如果删掉 Language,这段代码就无法运行
而且在作为静态类型使用的时候,enum 还会带来额外的心智负担
上面的 Language 如果换成联合类型的写法,可能更符合直觉:
type Language = 'zh_CN' | 'zh_HK' | 'zh_TW' | 'en_US' | 'en_GB';
最后,也是最大的缺点:由于使用了 enum,我们不得不使用 tsc 而非 ESbuild 来编译项目,导致整个编译过程的开销巨大
三、可选的替代方案
如果很在意编译过程的优化,可以考虑下面的替代方案
1. union type
type Language = 'zh_CN' | 'zh_HK' | 'zh_TW' | 'en_US' | 'en_GB';
function getLocals(lang: Language) {
return `hello ${lang}`;
}
getLocals('zh_CN');
这个方案简单粗暴,抛弃 enum 的特性,使用联合类型来代替枚举
其优点是通俗易懂,而且删掉类型后就是一段正常的 JS 代码
但缺点也很明显,不容易维护。假如需要将 'zh_CN' 改为 'zh_cn',那么所有用到了 'zh_CN' 的地方都要调整
2. object as const
const LangConstant = {
ZH_CN: 'zh_CN',
ZH_HK: 'zh_HK',
ZH_TW: 'zh_TW',
EN_US: 'en_US',
EN_GB: 'en_GB',
} as const;
type ValueOf<T> = T[keyof T];
type Language = ValueOf<typeof LangConstant>;
// "zh_CN" | "zh_HK" | "zh_TW" | "en_US" | "en_GB"
function getLocals(lang: Language) {
return `hello ${lang}`;
}
getLocals(LangConstant.ZH_CN);
getLocals('zh_CN'); // Language 是一个联合类型,所以这里并不会报错,但不推荐
直接创建一个 JS 对象来维护常量,这样就解决了方案一不易维护的问题
然后通过 keyof 和 typeof 获取到对象的值,并形成联合类型
这段代码删掉静态类型依然能够正常运行。除了实现上稍微有点复杂以外,是一个很不错的方案
不管是 union type 还是 object as const,其实都是对 enum 的吹毛求疵
如果项目不追求极致的编译优化,大可以放心使用 enum;如果不需要反向映射,使用 const enum 或许是一个最优解
P.S. 关于 enum 的小技巧
1. 获取枚举的 key 类型
type LangKeys = keyof typeof Language;
2. 获取枚举的 value 类型
type LangValues = `${Language}`;