写在前面
郑重承诺以下内容不由 AI 生成
前端里的“枚举”,本质上是在解决以下问题:
- 限制状态范围
- 提高可读性
- 提高类型安全
- 避免魔法字符串
- 方便 UI 映射
- 方便 AI 理解
下面作者为同学们总结了前端领域常见的枚举方式:
Typescript 的 enum 枚举类型
enum 是 Typescript 原生支持的枚举类型。许多同学不知道的是,枚举的定义方式是有区别的,定义方式会决定 enum 的应用方式,分为 enum 和 const enum
enum
支持定义数字和字符串,定义/使用方式如下:
enum Status {
Pending,
Success,
Failed
}
Status.Pending // 0
enum Status {
Pending = 'pending',
Success = 'success',
Failed = 'failed'
}
Status.Pending // pending
数字类型支持反向映射,如:
Status[Status.Pending] // Pending
字符串类型不支持反向映射,因为 enum 编译后会输出大致这样的代码:
// input
enum Status {
Pending,
Success,
Failed
}
// output
const Status = {
Pending: 0,
Success: 1,
Failed: 2,
0: "Pending",
1: "Success",
2: "Failed"
}
字符串会在设计层面有很多限制,这里随便举个例子:
// input
enum Vector {
X = 'Y',
Y = 'Z',
Z = 'X'
}
// output 命名空间已经明显乱掉了, 假设访问 Vector['X'],都不知道访问的是哪一个。
const Vector = {
X: 'Y',
Y: 'Z',
Z: 'X',
Y: 'X',
Z: 'Y',
X: 'Z'
}
const enum 枚举
上文的 enum 是包含 运行时 的,Typescript 会编译出一个 Javascript 对象,但对 bundle 尺寸和性能有要求的库,需要一种更轻的枚举类型,这就是后来 Typescript 团队支持的 const enum 特性。定义/使用方式如下:
const enum Status {
Pending,
Success,
Failed
}
Status.Pending // 0
它不支持双向映射,因为 ts 会在编译时将 Status 抹去,只把 Status.Pending 的调用直接替换成字面量 0。
对象枚举
绝大部分的前端工程师的工作还是以业务为主,更多只是单纯的寻求 好用 + 实用 + 类型安全,基本不会在意 bundle 尺寸,实际上对于 Web App 来说,枚举也不会带来明显的 bundle 尺寸增长。
这也催生出了对象枚举的定义方式,优势在于开发者可以自由的定义映射,类型使用也更加明确简单。
const Status = {
Pending: 'pending',
Success: 'success',
Failed: 'failed'
} as const
// as const 是必须的,影响下面的 type Status,对 as const 感兴趣的同学可以去问一下 AI,这里就不展开了。
const StatusLabels = {
[Status.Pending]: '准备中',
[Status.Success]: '成功',
[Status.Failed]: '失败',
}
// 这个类型工具可以提取成通用工具
type ValueOf<T> = T[keyof T]
type Status = ValueOf<typeof Status> // 'pending' | 'success' | 'failed'
Status.Pending // 'pending'
StatusLabels[Status.Pending] // '准备中'
enum-plus
对象枚举虽然一定程度的解决了业务问题,但定义起来实在有点复杂,虽然这对于 AI 来说不是什么难事,但枚举的信息密度非常低,不够内聚,关注点不够聚焦,AI 也经常犯迷糊。 但好在开发社区也有解决方案,比如 enum-plus。
import { Enum } from 'enum-plus'
const WeekEnum = Enum({
Sunday: { value: 0, label: 'I love Sunday' },
Monday: { value: 1, label: 'I hate Monday' },
});
WeekEnum.Sunday; // 0
WeekEnum.items[0].key; // 'Sunday'
WeekEnum.items[0].label; // 'I love Sunday'
作者找到这个库的时候也是想要直接集成到项目里的,因为它功能非常丰富,提供了 非常非常多 的 api。但同时我也觉得 api 过于复杂了,并且似乎类型约束相对松散,无法直接完成迁移(除非牺牲类型安全)比如:
import { Enum } from 'enum-plus'
const WeekEnum = Enum({
Sunday: { value: 0, label: 'I love Sunday' },
Monday: { value: 1, label: 'I hate Monday' },
})
console.log(WeekEnum.Sunday)
// 这里不会报类型错误,但 `2` 不是一个合法的枚举值,enum-plus 在这种情况下会返回 undefined,虽然也是合理的,但作者更倾向于严格一些类型约定。
console.log(WeekEnum.label(2))
enumOf
最后是作者自己的版本,个人比较满意。实现较为轻量,api 也比较简单,并且类型安全,国际化支持也非常容易,同时开源并集成到了 rattail 工具库里,有需要的同学可以自取。下面是常用案例。
import { enumOf } from 'rattail'
const Status = enumOf({
Success: { value: 0, label: 'Success' },
// 字段支持函数返回,国际化支持很简单。
Warning: { value: 1, label: () => t('global.warning') },
})
Status.Success // 0
Status.label(Status.Success) // 'Success'
Status.options()
/*
[
{ value: 0, label: 'Success'},
{ value: 1, label: 'Warning' },
]
*/
Status.values() // [0, 1]
Status.labels() // ['Success', 'Warning']
你也可以借助 rattail 内置的函数去调整 .options() 返回的数据结构,这个在业务开发里很常见。
import { rekey } from 'rattail'
Status.options().map(option => rekey(option, { value: 'key' }))
/*
[
{ key: 0, label: 'Success'},
{ key: 1, label: 'Warning' },
]
*/
你也可以选择扩展更多字段,它们都会有完善的类型推导。
const Status = enumOf({
Success: { value: 1, label: 'Success', color: 'green' },
Warning: { value: 2, label: 'Warning', color: 'orange' },
})
Status.option(Status.Success).color // 'green'
写在最后
感觉您耐心看完这篇文章,希望您能喜欢。这里是《前端毕业班》,前端开发者的自救互助小组。在 AI 与不确定性并存的时代,我们一起看清焦虑,聊技术、聊趋势,也聊前端还能走多远,走去哪。