TypeScript中,字符串字面量联合类型(Union Type)、enum的用法说明

1、使用字符串字面量联合类型(Union Type)

优点:零运行时开销

缺点:不支持重命名符号,重构不方便

TypeScript 复制代码
// 1. 使用字符串字面量联合类型(Union Type),优点:零运行时开销;缺点:不支持重命名符号,重构不方便
/**
 * 操作指令:新增删除:info-add;修改:info-modify;查看:info-view
 */
export type OperationCommand = "info-add" | "info-modify"| "info-view";

2、使用枚举 enum

优点:有利于重构,IDE 重命名符号

缺点:增加性能开销

TypeScript 复制代码
// 2. 使用枚举 enum,优点:有利于重构,IDE 重命名符号;缺点:增加性能开销
/**
 * 操作指令
 */
export enum OperationCommand {

  /**
   * 新增删除
   */
  ADD = "info-add",

  /**
   * 修改
   */
  MODIFY = "info-modify",

  /**
   * 查看
   */
  VIEW = "info-view"
}

3、使用 as const 常量对象 + 类型别名

零运行时开销,有利于重构,IDE 重命名符号

TypeScript 复制代码
// 3. 使用 as const 常量对象 + 类型别名(零运行时开销,有利于重构,IDE 重命名符号)
/**
 * 操作指令:新增删除:info-add;修改:info-modify;查看:info-view
 */
export const OperationCommand = {

  /**
   * 新增删除
   */
  ADD: "info-add",

  /**
   * 修改
   */
  MODIFY: "info-modify",

  /**
   * 查看
   */
  VIEW: "info-view"
} as const;
// 从常量对象推导出联合类型
export type OperationCommand = (typeof OperationCommand)[keyof typeof OperationCommand];

在 TypeScript 中,对于表示一组固定字符串值的场景,通常推荐使用字符串字面量联合类型(Union Type)而不是 enum。您当前的写法:

ts

复制代码
export type OperationCommand = "info-view" | "info-modify" | "info-add";

已经是比较现代且推荐的做法。


为什么字符串联合类型更好?

1. 零运行时开销

enum 会编译成一个真实的 JavaScript 对象(除非使用 const enum),增加代码体积。而字符串联合类型仅在编译时存在,编译后会被完全擦除,不会产生任何运行时代码。

ts

复制代码
// enum 编译后:
var OperationCommand;
(function (OperationCommand) {
    OperationCommand["info-view"] = "info-view";
    // ...
})(OperationCommand || (OperationCommand = {}));

// 字符串联合类型编译后:完全消失

2. 与 JavaScript 互操作性更好

字符串联合类型本质上就是普通字符串,可以直接与后端传来的字符串、环境变量、JSON 数据等交互,无需类型转换。而 enum 的值在某些场景下可能需要额外处理(如数字枚举)。字符串枚举虽然值是字符串,但类型名不同,跨模块使用时可能需要导入。

3. 简单直观,易于维护

联合类型一目了然,不需要关心 enum 的命名空间、反向映射等特性。添加或删除一个值只需修改类型定义,所有使用处会立即获得类型检查。

4. 现代 TypeScript 官方推荐

TypeScript 官方文档在"String Literal Types vs Enums"部分明确表示,使用字符串字面量联合类型通常是更好的选择,除非你有非常充足的理由使用 enum(例如需要迭代所有成员、希望有命名空间等)。


何时可以考虑使用 enum

  • 需要遍历所有枚举值(如生成下拉选项)。

  • 希望在运行时能够通过名字访问值 (如 OperationCommand["info-add"])。

  • 团队已经大量使用 enum,且希望保持一致。

但即便需要遍历,也可以使用 as const 对象配合类型推导来模拟 enum,同时保持零运行时开销和简洁性:

ts

复制代码
export const OperationCommand = {
  VIEW: "info-view",
  MODIFY: "info-modify",
  ADD: "info-add",
} as const;

export type OperationCommand = (typeof OperationCommand)[keyof typeof OperationCommand];

这样既拥有类似 enum 的命名空间和遍历能力,又避免了 enum 的额外运行时对象和潜在问题。


结论

您的现有代码(字符串联合类型)是正确且推荐的做法,无需改为 enum 。如果项目中没有特殊需求,继续使用字符串联合类型即可。只有在确实需要枚举的运行时特性时才考虑 enum,或者使用 as const 对象作为折中。


您的观察很准确:enum 在 VS Code 中确实提供了更好的"重命名符号"体验 ,因为枚举成员是明确的符号,使用 F2 重命名时会自动更新所有引用。而字符串联合类型的字面量分散在代码各处,重命名类型别名只能改类型定义,不会修改字符串值本身,需要手动查找替换。


如何兼顾:as const 对象 + 类型别名

采用 as const 常量对象 来定义命令值,再通过类型推导生成联合类型。这样既保留了 enum 的符号化引用(可重命名、可查找引用),又避免了 enum 的运行时开销和潜在问题。

示例

typescript

复制代码
// 定义常量对象,键名用大写标识符,值保持不变
export const OperationCommand = {
  VIEW: "info-view",
  MODIFY: "info-modify",
  ADD: "info-add",
} as const;

// 从常量对象推导出联合类型
export type OperationCommand = (typeof OperationCommand)[keyof typeof OperationCommand];

使用方式

typescript

复制代码
// 符号化引用(VS Code 可识别并支持重命名)
const cmd: OperationCommand = OperationCommand.ADD;

// 或者直接使用字面量(类型依然安全)
const cmd2: OperationCommand = "info-add";

优点

  1. 重命名方便 :选中 OperationCommand.ADDF2,可同时重命名所有使用该常量的地方。

  2. 保留类型安全:联合类型仍然有效,可直接使用字符串字面量。

  3. 零额外运行时开销as const 对象编译后就是一个普通对象(也可以被 tree-shaking 消除,如果未使用),不像 enum 会生成额外代码。

  4. 可遍历 :如果需要生成下拉选项等,可以直接 Object.values(OperationCommand)

  5. 与 JavaScript 互操作友好 :值就是普通字符串,没有 enum 的包装。


总结

  • 如果看重 重构便利性 ,且团队习惯使用 enum,可以继续使用 enum,因为它的成员是真正的符号,VS Code 支持完善。

  • 如果希望 避免 enum 的缺点 (运行时开销、与 JS 互操作性等),又想拥有类似的符号化能力,强烈推荐 as const 对象 + 类型别名 模式。

您可以根据项目实际情况和团队偏好选择。两种方式都能在 VS Code 中实现良好的重构体验。


使用示例(使用 as const 常量对象 + 类型别名):

示例1

TypeScript 复制代码
import { CommonOperationCommand } from "@/types";
provide(operationCommandSymbol, ref<CommonOperationCommand>("info-view"));

示例2

TypeScript 复制代码
import { CommonOperationCommand } from "@/types";
provide(operationCommandSymbol, ref<CommonOperationCommand>(CommonOperationCommand.VIEW));

是的,这样使用完全正确,而且比直接使用字符串字面量更好。

typescript

复制代码
import { CommonOperationCommand } from "@/types";
provide(operationCommandSymbol, ref<CommonOperationCommand>(CommonOperationCommand.VIEW));

为什么推荐这种用法?

  1. 符号化引用 :使用 CommonOperationCommand.VIEW 而不是字符串字面量 "info-view",让 VS Code 能够识别为符号,支持 F2 重命名查找所有引用,便于后续重构。

  2. 类型安全 :如果将来修改了 CommonOperationCommand.VIEW 的值(例如改为 "view"),所有使用该常量的地方会自动反映,而字符串字面量需要手动查找替换,容易遗漏。

  3. 避免拼写错误 :使用常量时,TypeScript 会在编译期检查成员名是否正确;而直接写字符串字面量时,如果拼写错误(如 "info-vuew"),虽然类型检查会报错(因为不是联合类型成员),但不如常量成员名直观。

与前一种用法的对比

前一种用法:

ts

复制代码
provide(operationCommandSymbol, ref<CommonOperationCommand>("info-view"));

也是合法的,因为 "info-view" 是联合类型的成员。但它的值是以裸字符串形式存在的,重构时不受保护。

需要注意的地方

  • 确保 CommonOperationCommand 同时导出了类型,当前定义已经满足。

  • 如果 tsconfig.json 开启了 verbatimModuleSyntaxisolatedModules,可能需要将类型导入分离:

    ts

    复制代码
    import { CommonOperationCommand } from "@/types"; // 导入值
    import type { CommonOperationCommand as CommonOperationCommandType } from "@/types"; // 导入类型
    // 使用时类型写作:ref<CommonOperationCommandType>

    但多数项目默认配置不会要求这样,可以直接混用。

总之,您给出的用法是标准且推荐的做法。


TypeScript 复制代码
import { CommonOperationCommand } from "@/types";
这里不是仅仅导入了值吗?

那为什么这里
provide(operationCommandSymbol, ref<CommonOperationCommand>(CommonOperationCommand.VIEW));
可以当作类型呢?ref<CommonOperationCommand>

这是 TypeScript 中很常见的同名导出 (value + type)用法,理解起来需要区分类型空间值空间

1. 同名导出

你的模块中同时导出了:

  • 一个export const CommonOperationCommand = { ... } as const;

  • 一个类型export type CommonOperationCommand = ...

这两个导出名称相同,但位于不同的"空间":

  • 值空间:变量、常量、函数、类实例等。

  • 类型空间:类型别名、接口、类类型等。

因为空间不同,所以可以共存,不会冲突。

2. 导入时的行为

import { CommonOperationCommand } from "@/types"; 会同时引入类型两个含义,因为同一个名字在两个空间中都有声明。

TypeScript 会根据使用位置自动决定取哪个:

  • 值位置 (如 CommonOperationCommand.VIEW)使用,就指(常量对象)。

  • 类型位置 (如 ref<CommonOperationCommand> 的尖括号内)使用,就指类型(联合类型)。

所以你的代码完全合法:

typescript

复制代码
provide(operationCommandSymbol, ref<CommonOperationCommand>(CommonOperationCommand.VIEW));
//                                     ↑ 类型位置:联合类型       ↑ 值位置:常量对象

3. 为什么不用分开导入?

默认 TypeScript 配置下,这种混合导入是允许的,TS 编译器能正确理解。但如果你开启了某些严格选项(如 isolatedModulesverbatimModuleSyntax),TS 可能会提示需要分开导入,因为它需要明确区分类型导入和值导入,以支持单文件转译工具(如 esbuild、swc)准确工作。

此时可以这样写:

typescript

复制代码
import { CommonOperationCommand } from "@/types"; // 值导入
import type { CommonOperationCommand as CommonOperationCommandType } from "@/types"; // 类型导入

// 使用类型:
provide(operationCommandSymbol, ref<CommonOperationCommandType>(CommonOperationCommand.VIEW));

但如果你没有启用这些选项,你的写法就是最简洁的,无需改动。

总结

  • import { CommonOperationCommand } 同时导入了值和类型。

  • TS 根据语法位置自动识别,你的用法完全正确。

  • 只需注意项目 tsconfig 是否启用了 verbatimModuleSyntax 等严格选项,如果有则需要分离导入。

相关推荐
jayson.h16 分钟前
PDF 合并+添加页码 相关库、类、函数
开发语言·前端·python
慧一居士1 小时前
Sass和Less功能、使用场景、用法对比
前端·css·less·sass
leoZ2311 小时前
AI+前端提效-09 AI赋能前端测试:单元测试、E2E测试自动生成,提升覆盖率
前端·人工智能·opencv·目标检测·数据挖掘·单元测试·语音识别
计算机魔术师2 小时前
OpenAI 评定 Astra 达到网络安全 Critical 能力阈值,将受限发布
前端
JavaGuide2 小时前
SpaceX 工程师的 AI Coding 玩法太牛了, 200 多个 Agent 并行!
前端·后端
小猪code2 小时前
frp 80/443 Web 服务两种部署模式实操笔记
前端·笔记
JamesZhang800782 小时前
Chrome相关知识点
前端
jayson.h2 小时前
python——pdf编辑
前端·python·pdf
顶级自由人2 小时前
本地正常、线上正常,为什么一个 Hook 仍会报错?
前端·javascript·程序员