前言:一场关于枚举的大型“真香”现场
还记得第一次写 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 中得到更好的提示,提升维护效率和开发体验。
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
中看出其结果。
然而反向映射
带来另外的问题就会导致编译后 代码体积膨胀,对于性能敏感的项目,这显然是不合适的。
类型问题
我们看下面的代码:
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
}
趋势
原文主要说到,
Typescript
从 5.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(联合数组)
, 这也是 const
和 enum
的共性问题。
扩展性
因为是对象结构,我们可以直接通过扩展运算符构造新的枚举。不会有像 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,
});
枚举扩展,配方纯正,类型安全!
终极比拼总结表出场
功能 | enum | const | enumily (推荐) |
---|---|---|---|
文档注释 | ✓ | ✓ | ✓ |
反向映射 | “冗余” | × | “随你控” |
类型推断粒度 | “一言难尽” | “95分” | “天花板级别” |
动态扩展 | × | ✓ | ✓ |
optionList 扩展 | “自个写去吧” | “手搓也行” | “自带+类型安全” |
工具方法丰富度 | × | × | “爆炸多” |
代码肥胖指数 | “易胖” | “超健康” | “健身达人型” |
结语
本文通过实际案例详细比较了 TypeScript
原生 enum
、const
对象和进阶枚举方案在项目开发中的优劣和适用场景。可以看到,虽然原生 enum
和 const
能在一定程度上满足基本需求,但在类型精确性、运行时能力和扩展性等方面仍存在不少局限。
随着项目和团队对类型安全、易维护性的要求不断提升,一个更现代、更完善的枚举工具显得很有必要。enumily
正是为了解决这些痛点而生:它在类型推断、运行时扩展、辅助方法等方面都进行了优化,能够让你在实际开发中更简单高效地进行枚举相关工作。
如果你在实际开发中也遇到类似问题,不妨试试 enumily
,它或许能给你的项目带来意想不到的提升。
enumily 仓库地址(GitHub) 欢迎 Star、提 Issue 交流。