在 Vue 3 + TypeScript 项目中,enum 的选择不仅涉及类型定义,还涉及 Vue SFC 的编译规则、模块导出方式,以及 Vite 单文件转译带来的限制。
本文从跨模块导入与导出、编译语义、方案选型三个方面,总结实际开发中的使用经验。
一、跨模块导入与导出
1.1 .vue 文件与 .ts 文件的区别
在 Vue 组件中,私有枚举可以直接声明在 <script setup lang="ts"> 中:
html
<script setup lang="ts">
enum Status {
Pending = 0,
Success = 1,
}
const status = Status.Pending
</script>
<template>
<div v-if="status === Status.Pending">等待中</div>
<div v-else>已完成</div>
</template>
<script setup> 中的顶层绑定可以直接在模板中使用,普通 enum 和 const enum 都能被 Vue 编译器识别为模板绑定。需要注意,编译器能识别绑定,不等于所有构建链都能正确处理 const enum。
1.2 跨组件共享:优先在 .ts 中声明
当枚举需要被多个组件、工具函数或业务模块使用时,建议将它独立为一个 TypeScript 模块。
ts
// src/enums/order.ts
export enum OrderStatus {
Pending = 'pending',
Processing = 'processing',
Completed = 'completed',
}
Vue 组件中通过普通值导入使用:
html
<script setup lang="ts">
import { ref } from 'vue'
import { OrderStatus } from '@/enums/order'
const status = ref(OrderStatus.Pending)
</script>
<template>
<div v-if="status === OrderStatus.Completed">
订单已完成
</div>
</template>
这里需要使用普通的 import,因为模板要访问 OrderStatus.Completed 这个运行时值。
如果只需要枚举类型,而不需要访问枚举成员,则可以在纯 .ts 文件中使用 import type:
ts
import type { OrderStatus } from '@/enums/order'
function handleStatus(status: OrderStatus) {
// 仅在类型层面使用 status
}
1.3 为什么不建议直接从 <script setup> 导出枚举?
<script setup> 不是普通的 ES 模块脚本区域,不支持直接通过顶层 export 导出声明:
ts
<script setup lang="ts">
// 不要这样写
export enum Status {
Pending = 0,
Success = 1,
}
</script>
如果确实需要命名导出,可以使用普通 <script>,但这会让组件同时承担组件定义和公共模块导出的职责,增加维护成本。
更清晰的结构是:
css
src/
├── enums/
│ └── order.ts
├── components/
│ └── OrderStatus.vue
└── index.ts
通过独立模块维护枚举,组件只负责展示和交互。如果项目需要统一导出,也可以在入口文件中转发:
ts
export { OrderStatus } from './enums/order'
export { default as OrderStatusBadge } from './components/OrderStatus.vue'
实践原则:组件内部使用的枚举可以就近声明;跨模块共享的枚举应优先放在 .ts 文件中。
二、enum 的编译语义
2.1 普通 enum 会生成运行时对象
例如:
ts
enum Direction {
Up,
Down,
}
普通数字枚举编译后会生成包含正向映射和反向映射的对象。大致可以理解为:
ts
Direction[Direction['Up'] = 0] = 'Up'
Direction[Direction['Down'] = 1] = 'Down'
2.2 const enum 会内联成员值
ts
const enum Direction {
Up = 0,
Down = 1,
}
const direction = Direction.Up
在 TypeScript 的传统编译行为下,成员引用会被直接替换为常量值,通常不会生成完整的枚举运行时对象:
ts
const direction = 0 /* Direction.Up */
这样可以减少生成的代码,但也带来了限制:不能依赖它一定存在可供外部访问的运行时对象。
Vue 官方编译器能够识别 <script setup> 中的 const enum,但 Vue SFC 的识别能力不能替代对整个构建链的验证。
2.3 Vite、isolatedModules 与跨模块限制
Vue + Vite 项目通常采用单文件转译方式处理 TypeScript。官方 Vue TypeScript 指南建议启用 isolatedModules,或者采用包含相应约束的 verbatimModuleSyntax 配置。
单文件转译器无法像完整的 TypeScript 编译器那样跨模块分析所有类型信息,因此 const enum 尤其需要谨慎。
主要风险包括:
- 跨模块内联: 消费方可能需要枚举的实际成员值,但转译器不一定能获取这些值。
- 类型声明发布: 发布到 npm 的声明文件如果包含环境枚举形式的
const enum,可能给使用不同编译配置的消费者带来问题。 - 版本不一致: 消费方编译时内联的值,可能与运行时加载的依赖版本不一致。
因此,项目内部单一构建链下的 const enum,与公共库跨模块发布的 const enum,不应该采用相同的风险判断。
三、方案选型:enum、const enum 与 as const
3.1 三种方案的区别
| 对比项 | enum |
const enum |
as const 对象 |
|---|---|---|---|
| 运行时对象 | 有 | 通常没有独立对象 | 有 |
| 模板访问 | 支持 | 受编译链影响 | 支持 |
| 类型用途 | 支持 | 支持 | 通过 typeof 推导 |
| 数字反向映射 | 支持 | 不适用独立对象访问 | 不支持 |
| 跨模块兼容性 | 较好 | 需要额外评估 | 较好 |
| 推荐场景 | 常规枚举 | 可控的内部优化场景 | 前端状态、常量集合 |
3.2 什么时候使用普通 enum?
当需要明确的枚举类型、运行时成员访问,或者希望通过数字枚举的反向映射获取成员名称时,可以选择普通 enum。
ts
export enum OrderStatus {
Pending = 0,
Completed = 1,
}
它的优点是语义明确,且对运行时访问和跨模块导入的支持相对直接。
3.3 什么时候使用 as const?
对于 Vue 前端项目中的状态值、选项集合、接口字段映射等场景,as const 对象往往是更灵活的替代方案。
ts
export const OrderStatus = {
Pending: 'pending',
Completed: 'completed',
} as const
export type OrderStatus =
(typeof OrderStatus)[keyof typeof OrderStatus]
这里有两个不同的导出:
OrderStatus:运行时对象,可用于模板和业务逻辑。OrderStatus类型:通过对象值推导出的字面量联合类型,可用于参数、变量和接口定义。
使用方式:
html
<script setup lang="ts">
import { ref } from 'vue'
import { OrderStatus } from '@/enums/order'
const status = ref(OrderStatus.Pending)
</script>
<template>
<div v-if="status === OrderStatus.Completed">
已完成
</div>
</template>
在纯类型场景中,则可以写:
ts
import type { OrderStatus } from '@/enums/order'
function updateStatus(status: OrderStatus) {
// status 只能是 'pending' 或 'completed'
}
注意,as const 只影响 TypeScript 类型推导,不会自动对外部接口返回的数据进行运行时校验。
3.4 最终选型建议
- 组件私有枚举: 可以直接在
<script setup lang="ts">中声明。 - 跨模块共享枚举: 默认使用普通
enum,或者使用as const对象。 - 需要运行时对象: 不要把
const enum当作普通对象使用。 - 公共 npm 包或多构建工具项目: 避免未经验证地发布环境
const enum。 - 前端状态和接口常量: 优先考虑
as const对象,便于同时获得运行时值和精确的字面量类型。
总结
选择 enum 时,核心不是追求最少的生成代码,而是明确三个边界:声明位置决定模块组织方式,编译语义决定运行时行为,使用场景决定最终选型。
对大多数 Vue 3 + TypeScript 项目而言,普通 enum 和 as const 对象已经可以覆盖绝大多数需求;只有在清楚掌握编译链、类型声明发布方式和运行时边界后,才值得引入 const enum。