Talk is cheap. Show me your code

TypeScript —— 枚举类型 enum 的红与黑

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}`;

 

posted @ 2022-06-14 18:43  Wise.Wrong  阅读(2327)  评论(1编辑  收藏  举报