别再用TypeScript enum 啦!前端枚举的现代打开方式

3,256 阅读8分钟

前言:一场关于枚举的大型“真香”现场

还记得第一次写 TypeScript 枚举时的感觉吗? ——哇!居然有 enum,这下人生哲学(代码分支)终于能用名字说话了!

结果没过多久,你发现:

  • 枚举类型蹦跶出反向映射,瞬间代码体重暴增,内心 OS:我好像养了个定时炸弹?
  • 当你想动态加几个成员:抱歉,enum 表示“我生下来就这样,想扩容?重新投胎!”
  • 类型推导、optionList、类型安全你追我赶,一不小心递归到自己晕头转向……

作为多年企业级项目打工人,我已经和 enum 彻底“分手”,但回首往事还是忍不住吐槽一番,同时强烈推荐终极神器——enumily,让你在枚举的江湖稳稳当当、优雅起飞~

如果你是老牛马,喜欢一目十行,可以直接跳到【总结对比】看 ending。如果你是枚举新手或者有社恐,别慌,跟着我愉快玩耍,每一步都配备“防爆小妙招+真实翻车故事”!

文章代码较多,如果你已经是一位资深老牛马,可能会有些许枯燥,可直接移步至文章最后部分,哈哈 😎

为了方便描述和对比,我将业务开发中常见的枚举使用方式抽象为一个简单的案例:假设我们现在需要一个关于方向的枚举 DirectionEnum,在项目开发过程中,我们通常需要依赖该枚举获取以下信息:

  • 获取枚举键类型
  • 获取枚举值类型
  • 获取所有的键
  • 获取所有的值
  • 获取枚举个数
  • 生成下拉选项数据
  • 扩展新的枚举
  • ……

先来排排座:原生 enum 的本事与“中年危机”

咱们先看 TypeScript 官宣的“枚举选美冠军”——enum选手:

enum DirectionNumber {
  /** 上:1 */
  Up = 1,
  /** 下:2 */
  Down= 2,
  /** 左:3 */
  Left= 3,
  /** 右 */
  Right= 4,
}

enum DirectionString {
  /** 上:up */
  Up = 'up',
  /** 下:down */
  Down= 'down',
  /** 左:left */
  Left= 'left',
  /** 右:right */
  Right= 'right',
}

注意上面我对每个枚举都进行了注释,这也是 enum 的优势之一,可以进行 文档注释。这应该是一个良好的开发习惯,这么做的好处是能够在 IDE 中得到更好的提示,提升维护效率和开发体验。

image.png

1. 枚举键类型获取,so easy?

type DirectionNumberKey = keyof typeof DirectionNumber;
// "Up" | "Down" | "Left" | "Right"

type DirectionStringKey = keyof typeof DirectionString;
// "Up" | "Down" | "Left" | "Right"

通过 typeof 得到枚举的类型, 通过 keyof 得到枚举的键类型。

2. 枚举值类型?用反引号耍花枪还是“逆天改命”?

type DirectionNumberValue = `${DirectionNumber}`
// "1" | "2" | "3" | "4"

// 或
type DirectionNumberValue = typeof DirectionNumber[keyof typeof DirectionNumber];
// 指向 DirectionNumber, 不推荐


type DirectionStringValue = `${DirectionString}`
// "up" | "down" | "left" | "right"

// 或
type DirectionStringValue = typeof DirectionString[keyof typeof DirectionString];
// 指向 DirectionString, 不推荐

看着好像没毛病,但实际开发遇到鬼畜情况,类型推断立刻就撂挑子(数字字符串混淆、鸭子类型横行)。

3. 获取所有 key,注意余震!

数字 enum 最大的“彩蛋”就是自动反向映射:键和值都能“变身”,debug 一不小心掉进无底洞。

const DirectionNumberKeys = Object.keys(DirectionNumber)
.filter((key) => isNaN(Number(key))) as (keyof typeof DirectionNumber)[];
// [ 'Up', 'Down', 'Left', 'Right' ]

const DirectionStringKeys = Object.keys(DirectionString) 
as (keyof typeof DirectionString)[];
// ['Up', 'Down', 'Left', 'Right']

小心啊!没有 Number(key) 检查,你都不知道自己搞出来多少无用数字字符串……

4. 获取所有 value,筛选走丢不怪他!

const DirectionNumberLength = Object.keys(DirectionNumber)
.filter((key) => isNaN(Number(key))).length;
// 4

const DirectionStringLength = Object.keys(DirectionString).length;
// 4

为啥多了一步 typeof?要感谢 enum 的原生反向映射机制,脑筋急转弯。

5. 个数、optionList 你都得小心翼翼

获取个数:

const DirectionNumberLength = DirectionNumberKeys.length; // 4

组装选项:

const DirectionNumberOptions = [
  { key: 'UP', value: DirectionNumber.Up, label: '上' },
  { key: 'Down', value: DirectionNumber.Down, label: '下' },
  { key: 'Left', value: DirectionNumber.Left, label: '左' },
  { key: 'Right', value: DirectionNumber.Right, label: '右' },
] as const;
//[
//  { key: 'UP', value: 1, label: '上' },
//  { key: 'Down', value: 2, label: '下' },
//  { key: 'Left', value: 3, label: '左' },
//  { key: 'Right', value: 4, label: '右' }
// ]

const DirectionStringOptions = [
  { key: 'Up', value: DirectionString.Up, label: '上' },
  { key: 'Down', value: DirectionString.Down, label: '下' },
  { key: 'Left', value: DirectionString.Left, label: '左' },
  { key: 'Right', value: DirectionString.Right, label: '右' },
] as const;
//[
//  { key: 'Up', value: 'up', label: '上' },
//  { key: 'Down', value: 'down', label: '下' },
//  { key: 'Left', value: 'left', label: '左' },
//  { key: 'Right', value: 'right', label: '右' }
//]

纯手工打造,坚持多年来不改配方~

6. 动态扩展?enum:对不起,这个锅我背不起

你以为加个新方向很简单? enum:“我闭合类型概念很强,你别想搞事情。”

enum NewDirection {
  Up = 1,
  Down = 2,
  Left = 3,
  Right = 4,
  LeftTop = 5,
  // ...
}

总结

反向映射问题

通过上面案例不难发现,对于涉及数字值的枚举需要进行特殊处理。这是因为 Typescript 对于数字枚举会生成 反向映射DirectionNumber 编译后生成的对象如下:

{
  '1': 'Up',
  '2': 'Down',
  '3': 'Left',
  '4': 'Right',
  Up: 1,
  Down: 2,
  Left: 3,
  Right: 4
}

那这个反向映射有什么用呢 ?一方面,在一定程度上简化了代码,可以直接通过 value 得到 key,另一方面就是方便调试, 比如日志场景,因为通过数字无法看出其代表的含义,但是枚举的键通常都会包含语义信息。但是比较遗憾的是无法像键那样直接在 IDE 中看出其结果。

image.png

image.png

然而反向映射带来另外的问题就会导致编译后 代码体积膨胀,对于性能敏感的项目,这显然是不合适的。

类型问题

我们看下面的代码:

type DirectionNumberValue = typeof DirectionNumber[keyof typeof DirectionNumber]; 
// 指向 DirectionNumber

function toDirectionNumber(direction: DirectionNumberValue) {}
toDirectionNumber(1) // ok
toDirectionNumber(DirectionNumber.Up) // ok

type DirectionStringValue = typeof DirectionString[keyof typeof DirectionString]; 
// 指向 DirectionString

function toDirectionString(direction: DirectionStringValue) {}
toDirectionString('up') //报错
toDirectionString2(DirectionString.Up) // ok

可以发现,数字枚举和字符串枚举的表现不一。对于字符串枚举,直接传入值 up 会抛出错误,但数字枚举却不会报错。对于这种不一致性,会增加我们项目的 维护和开发难度

扩展性问题

TypeScript 中的 enum 是一种闭合类型,不能动态扩展枚举。如果我们要基于现有的枚举扩展新的枚举,只能拷贝一份。这样就会导致不仅占用了更多的内存,同样的枚举也维护了多份,一旦在业务迭代中需要对枚举进行变更,很有可能因为疏忽遗漏导致线上故障。

enum Direction {
  /** 上:1 */
  Up = 1,
  /** 下:2 */
  Down= 2, 
  /** 左:3 */ 
  Left= 3, 
  /** 右 */ 
  Right= 4, 
}

enum NewDirection {
  /** 上:1 */
  Up = 1,
  /** 下:2 */
  Down= 2, 
  /** 左:3 */ 
  Left= 3, 
  /** 右 */ 
  Right= 4, 
  /** 左上 */
  LeftTop: 5,
  /** 左下 */
  LeftDown: 6,
  /** 右上 */
  RightTop: 7,
  /** 右下 */
  RightDown: 8
}

趋势

image.png 原文主要说到,Typescript5.8 引入了 --erasableSyntaxOnly 标记,开启该标记会引起 enum 不可用。

const as const 对象:修身养性,一路轻盈~

不想再玩 enum 俄罗斯套娃?考虑下 const + as const 治愈一切:

const DirectionNumber = {
  /** 上:1 */
  Up: 1,
  /** 下:2 */
  Down: 2,
  /** 左:3 */
  Left: 3,
  /** 右 */
  Right: 4,
} as const


const DirectionString = {
  /** 上:up */
  Up: 'up',
  /** 下:down */
  Down: 'down',
  /** 左:left */
  Left: 'left',
  /** 右:right */
  Right: 'right',
} as const

优点三板斧

  • 体积瘦身,没有反向映射的肥胖困扰
  • 结构灵活,类型推断良心到家
  • 可以用对象展开随意扩展——进化能力点满

常规操作回顾

type DirectionNumberKey = keyof typeof DirectionNumber;
type DirectionNumberValue = typeof DirectionNumber[DirectionNumberKey];

const DirectionNumberKeys = Object.keys(DirectionNumber) as Array<DirectionNumberKey>;
const DirectionNumberValues = Object.values(DirectionNumber);

optionList&动态扩展,体验爽爆!

const DirectionNumberOptions = [
  { key: 'Up', value: DirectionNumber.Up, label: '上' },
  // ...
];

const NewDirection = {
  ...DirectionString,
  LeftTop: 'left-top',
  // ...
} as const;

再也不用担心,因为“复写”而遗失某个 key!

温馨提示

唯一难受的小感冒:keys/values 默认只推到 Array<string | number>,如要 tuple 严格类型,还得鼓捣 Type 操作。

总结

性能

可以看到用 const 替代 enum, 没有反向映射问题,不会增加代码体积。

类型

通过 as const 延申得到的联合类型,不会再像 enum 那样有歧义。

但是依然存在不足之处,比如 DirectionNumberKeys 类型是 ("Up" | "Down" | "Left" | "Right")[]DirectionStringLength 类型是 number 等等。因为枚举本身就是静态数据,这里期望的键类型应该是 Tuple(元组) 而不是 Union Array(联合数组), 这也是 constenum 的共性问题。

扩展性

因为是对象结构,我们可以直接通过扩展运算符构造新的枚举。不会有像 enum 那样维护多份数据的问题。

enumily!闪亮登场

经过顶部两位选手毫无掩饰的“鸡毛蒜皮”,让我们隆重请出登峰造极的“enumily”!

好用到哭的 API 格式

import { createEnum } from 'enumily';

const DirectionEnum = createEnum({
  /** 向上:1 */
  Up: 1,
  /** 向下:2 */
  Down: 2,
  /** 向左:3 */
  Left: 3,
  /** 向右 */
  Right: 4,
});

类型推断爆表,自动变聪明!

import { EnumKey, EnumValue } from 'enumily';

type DirectionKey = EnumKey<typeof DirectionEnum>;   // "Up" | "Down" | "Left" | "Right"
type DirectionValue = EnumValue<typeof DirectionEnum>; // 1 | 2 | 3 | 4

切片、合成、Map 转换一把梭

const DirectionEnumKeys = DirectionEnum.$getKeys(); // ['Up', ...] 严格 tuple!
const DirectionEnumValues = DirectionEnum.$getValues(); // [1, 2, 3, 4]
const DirectionEnumLength = DirectionEnum.$length; // 4

optionList 生成,优雅到每个 key 都会跳舞

const DirectionEnumOptionList = DirectionEnum.$toList([
  [DirectionEnum.Up, '上'],
  [DirectionEnum.Down, '下'],
  [DirectionEnum.Left, '左'],
  [DirectionEnum.Right, '右'],
]);

动态扩展?一分钟 get 新技能!

const NewDirectionEnum = DirectionEnum.$extend({
  LeftTop: 5,
  LeftDown: 6,
  RightTop: 7,
  RightDown: 8,
});

枚举扩展,配方纯正,类型安全!

终极比拼总结表出场

功能enumconstenumily(推荐)
文档注释
反向映射“冗余”ד随你控”
类型推断粒度“一言难尽”“95分”“天花板级别”
动态扩展×
optionList 扩展“自个写去吧”“手搓也行”“自带+类型安全”
工具方法丰富度×ד爆炸多”
代码肥胖指数“易胖”“超健康”“健身达人型”

结语

本文通过实际案例详细比较了 TypeScript 原生 enumconst 对象和进阶枚举方案在项目开发中的优劣和适用场景。可以看到,虽然原生 enumconst 能在一定程度上满足基本需求,但在类型精确性、运行时能力和扩展性等方面仍存在不少局限。

随着项目和团队对类型安全、易维护性的要求不断提升,一个更现代、更完善的枚举工具显得很有必要。enumily 正是为了解决这些痛点而生:它在类型推断、运行时扩展、辅助方法等方面都进行了优化,能够让你在实际开发中更简单高效地进行枚举相关工作。

如果你在实际开发中也遇到类似问题,不妨试试 enumily,它或许能给你的项目带来意想不到的提升。

enumily 仓库地址(GitHub) 欢迎 Star、提 Issue 交流。