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";
优点
-
重命名方便 :选中
OperationCommand.ADD按F2,可同时重命名所有使用该常量的地方。 -
保留类型安全:联合类型仍然有效,可直接使用字符串字面量。
-
零额外运行时开销 :
as const对象编译后就是一个普通对象(也可以被 tree-shaking 消除,如果未使用),不像enum会生成额外代码。 -
可遍历 :如果需要生成下拉选项等,可以直接
Object.values(OperationCommand)。 -
与 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));
为什么推荐这种用法?
-
符号化引用 :使用
CommonOperationCommand.VIEW而不是字符串字面量"info-view",让 VS Code 能够识别为符号,支持 F2 重命名 和 查找所有引用,便于后续重构。 -
类型安全 :如果将来修改了
CommonOperationCommand.VIEW的值(例如改为"view"),所有使用该常量的地方会自动反映,而字符串字面量需要手动查找替换,容易遗漏。 -
避免拼写错误 :使用常量时,TypeScript 会在编译期检查成员名是否正确;而直接写字符串字面量时,如果拼写错误(如
"info-vuew"),虽然类型检查会报错(因为不是联合类型成员),但不如常量成员名直观。
与前一种用法的对比
前一种用法:
ts
provide(operationCommandSymbol, ref<CommonOperationCommand>("info-view"));
也是合法的,因为 "info-view" 是联合类型的成员。但它的值是以裸字符串形式存在的,重构时不受保护。
需要注意的地方
-
确保
CommonOperationCommand同时导出了值 和类型,当前定义已经满足。 -
如果
tsconfig.json开启了verbatimModuleSyntax或isolatedModules,可能需要将类型导入分离: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 编译器能正确理解。但如果你开启了某些严格选项(如 isolatedModules 或 verbatimModuleSyntax),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等严格选项,如果有则需要分离导入。