Vue 3 + TypeScript 中 enum 的使用实践

在 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。

相关推荐
鸥梨菌Honevid1 小时前
UVM 中 REQ 和 RSP 类型详解:从通信模型到 item_done 与 put_response
java·前端·servlet
IT_陈寒1 小时前
Python多进程共享变量这个坑,差点让我加班到凌晨
前端·人工智能·后端
鸿蒙开发1 小时前
PDF 盖黑框不等于脱敏:用 PDF.js + Canvas + pdf-lib 实现永久遮盖
前端·react.js
xixingzhe21 小时前
Nginx 看懂请求耗时瓶颈
服务器·前端·nginx
Lank_M1 小时前
前端AVIF编码方案评审,九个想当然逐个实测
前端
程序员鱼皮1 小时前
我用 REA 逆向了付费颜色 APP,结果很意外!
前端
志尊宝1 小时前
Vue3 零基础每日笔记(087):Vue3 高频面试题(下)
前端·javascript·vue.js·笔记·ecmascript·#vue.js
pusheng20252 小时前
普晟传感再获“消防产业核心配套优质品牌”:以可靠传感技术,让全球合规变简单
大数据·前端·人工智能·物联网·安全
Java后端的Ai之路2 小时前
15_ReactJS_Flux_简介
前端·react.js·前端框架