Vue 3 + TypeScript 中 enum 的使用实践

36 阅读6分钟

在 Vue 3 + TypeScript 项目中,enum 的选择不仅涉及类型定义,还涉及 Vue SFC 的编译规则、模块导出方式,以及 Vite 单文件转译带来的限制。

本文从跨模块导入与导出、编译语义、方案选型三个方面,总结实际开发中的使用经验。

一、跨模块导入与导出

1.1 .vue 文件与 .ts 文件的区别

在 Vue 组件中,私有枚举可以直接声明在 <script setup lang="ts"> 中:

<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 模块。

// src/enums/order.ts
export enum OrderStatus {
  Pending = 'pending',
  Processing = 'processing',
  Completed = 'completed',
}

Vue 组件中通过普通值导入使用:

<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:

import type { OrderStatus } from '@/enums/order'

function handleStatus(status: OrderStatus) {
  // 仅在类型层面使用 status
}

1.3 为什么不建议直接从 <script setup> 导出枚举?

<script setup> 不是普通的 ES 模块脚本区域,不支持直接通过顶层 export 导出声明:

<script setup lang="ts">
// 不要这样写
export enum Status {
  Pending = 0,
  Success = 1,
}
</script>

如果确实需要命名导出,可以使用普通 <script>,但这会让组件同时承担组件定义和公共模块导出的职责,增加维护成本。

更清晰的结构是:

src/
├── enums/
│   └── order.ts
├── components/
│   └── OrderStatus.vue
└── index.ts

通过独立模块维护枚举,组件只负责展示和交互。如果项目需要统一导出,也可以在入口文件中转发:

export { OrderStatus } from './enums/order'
export { default as OrderStatusBadge } from './components/OrderStatus.vue'

实践原则:组件内部使用的枚举可以就近声明;跨模块共享的枚举应优先放在 .ts 文件中。

二、enum 的编译语义

2.1 普通 enum 会生成运行时对象

例如:

enum Direction {
  Up,
  Down,
}

普通数字枚举编译后会生成包含正向映射和反向映射的对象。大致可以理解为:

Direction[Direction['Up'] = 0] = 'Up'
Direction[Direction['Down'] = 1] = 'Down'

2.2 const enum 会内联成员值

const enum Direction {
  Up = 0,
  Down = 1,
}

const direction = Direction.Up

在 TypeScript 的传统编译行为下,成员引用会被直接替换为常量值,通常不会生成完整的枚举运行时对象:

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 三种方案的区别

对比项enumconst enumas const 对象
运行时对象有通常没有独立对象有
模板访问支持受编译链影响支持
类型用途支持支持通过 typeof 推导
数字反向映射支持不适用独立对象访问不支持
跨模块兼容性较好需要额外评估较好
推荐场景常规枚举可控的内部优化场景前端状态、常量集合

3.2 什么时候使用普通 enum?

当需要明确的枚举类型、运行时成员访问,或者希望通过数字枚举的反向映射获取成员名称时,可以选择普通 enum。

export enum OrderStatus {
  Pending = 0,
  Completed = 1,
}

它的优点是语义明确,且对运行时访问和跨模块导入的支持相对直接。

3.3 什么时候使用 as const?

对于 Vue 前端项目中的状态值、选项集合、接口字段映射等场景,as const 对象往往是更灵活的替代方案。

export const OrderStatus = {
  Pending: 'pending',
  Completed: 'completed',
} as const

export type OrderStatus =
  (typeof OrderStatus)[keyof typeof OrderStatus]

这里有两个不同的导出:

  • OrderStatus:运行时对象,可用于模板和业务逻辑。
  • OrderStatus 类型:通过对象值推导出的字面量联合类型,可用于参数、变量和接口定义。

使用方式:

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