字符串枚举
概述
字符串枚举(String Enums)是 TypeScript 中一种特殊的枚举类型,其成员值是字符串字面量。与数字枚举不同,字符串枚举的每个成员都必须显式指定字符串值,不支持自动递增。字符串枚举提供了更好的调试体验(显示有意义的字符串而非数字)和更严格的类型检查,特别适合用于 API 端点、配置键、权限标识等场景。
字符串枚举在编译后会生成 JavaScript 对象,但不支持反向映射,这是与数字枚举的重要区别。字符串枚举更适合需要序列化、日志记录或与外部系统交互的场景。
基本特性
必须显式指定值
字符串枚举的每个成员都必须显式指定字符串值,不能像数字枚举那样自动递增:
typescript
// ✅ 正确:字符串枚举必须为每个成员指定值
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
console.log(Direction.Up); // "UP"
console.log(Direction.Down); // "DOWN"
console.log(Direction.Left); // "LEFT"
console.log(Direction.Right); // "RIGHT"typescript
// ❌ 错误:字符串枚举不能自动递增
enum Color {
Red, // Error: Enum member must have initializer
Green = "green",
Blue = "blue"
}字符串字面量值
字符串枚举的成员值必须是字符串字面量,可以是任意字符串:
typescript
// 使用大写标识符
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Delete = "DELETE",
Patch = "PATCH"
}
// 使用小写标识符
enum LogLevel {
Debug = "debug",
Info = "info",
Warn = "warn",
Error = "error"
}
// 使用描述性字符串
enum UserRole {
Administrator = "administrator",
Editor = "editor",
Viewer = "viewer",
Guest = "guest"
}
// 使用短标识符
enum Status {
Pending = "pending",
Active = "active",
Inactive = "inactive",
Deleted = "deleted"
}不支持反向映射
与数字枚举不同,字符串枚举不支持反向映射:
typescript
enum Direction {
Up = "UP",
Down = "DOWN"
}
// ✅ 正向访问:通过名称获取值
console.log(Direction.Up); // "UP"
console.log(Direction.Down); // "DOWN"
// ❌ 反向映射:字符串枚举不支持
console.log(Direction["UP"]); // undefined(不存在)
console.log(Direction["DOWN"]); // undefined(不存在)
// 编译后的 JavaScript 代码(简化版)
var Direction;
(function (Direction) {
Direction["Up"] = "UP";
Direction["Down"] = "DOWN";
})(Direction || (Direction = {}));
// 注意:没有反向映射,只有 Direction.Up → "UP"注意
字符串枚举不支持反向映射,因为字符串值不能作为对象的数字键。如果需要通过值获取名称,需要手动实现映射逻辑。
使用示例
示例 1:HTTP 方法枚举
使用字符串枚举定义 HTTP 方法,适合与 REST API 交互:
typescript
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Delete = "DELETE",
Patch = "PATCH",
Head = "HEAD",
Options = "OPTIONS"
}
interface ApiRequest {
method: HttpMethod;
url: string;
body?: any;
}
class HttpClient {
async request(config: ApiRequest): Promise<Response> {
const { method, url, body } = config;
const options: RequestInit = {
method, // 直接使用枚举值作为字符串
headers: {
'Content-Type': 'application/json'
}
};
if (body && (method === HttpMethod.Post || method === HttpMethod.Put || method === HttpMethod.Patch)) {
options.body = JSON.stringify(body);
}
return fetch(url, options);
}
// 便捷方法
get(url: string): Promise<Response> {
return this.request({ method: HttpMethod.Get, url });
}
post(url: string, body: any): Promise<Response> {
return this.request({ method: HttpMethod.Post, url, body });
}
delete(url: string): Promise<Response> {
return this.request({ method: HttpMethod.Delete, url });
}
}
const client = new HttpClient();
// 使用枚举值
client.get('/api/users');
client.post('/api/users', { name: 'John' });
client.delete('/api/users/123');
// 枚举值直接作为字符串使用
console.log(HttpMethod.Get); // "GET"
console.log(HttpMethod.Post); // "POST"示例 2:用户权限系统
使用字符串枚举定义用户权限,便于序列化和传输:
typescript
enum Permission {
Read = "read",
Write = "write",
Delete = "delete",
Admin = "admin"
}
interface User {
id: number;
name: string;
permissions: Permission[];
}
class PermissionManager {
// 检查用户是否有特定权限
hasPermission(user: User, permission: Permission): boolean {
return user.permissions.includes(permission);
}
// 检查用户是否有任一权限
hasAnyPermission(user: User, permissions: Permission[]): boolean {
return permissions.some(perm => this.hasPermission(user, perm));
}
// 检查用户是否有所有权限
hasAllPermissions(user: User, permissions: Permission[]): boolean {
return permissions.every(perm => this.hasPermission(user, perm));
}
// 添加权限
addPermission(user: User, permission: Permission): void {
if (!this.hasPermission(user, permission)) {
user.permissions.push(permission);
}
}
// 移除权限
removePermission(user: User, permission: Permission): void {
user.permissions = user.permissions.filter(p => p !== permission);
}
// 检查是否可以执行删除操作
canDelete(user: User): boolean {
return this.hasPermission(user, Permission.Delete) ||
this.hasPermission(user, Permission.Admin);
}
}
const manager = new PermissionManager();
const user: User = {
id: 1,
name: "Alice",
permissions: [Permission.Read, Permission.Write]
};
const admin: User = {
id: 2,
name: "Bob",
permissions: [Permission.Admin]
};
console.log(manager.hasPermission(user, Permission.Read)); // true
console.log(manager.hasPermission(user, Permission.Delete)); // false
console.log(manager.canDelete(user)); // false
console.log(manager.canDelete(admin)); // true
// 权限可以轻松序列化为 JSON
const userJson = JSON.stringify(user);
console.log(userJson);
// {"id":1,"name":"Alice","permissions":["read","write"]}
// 从 JSON 反序列化时,字符串值可以直接使用
const parsedUser: User = JSON.parse(userJson);
console.log(parsedUser.permissions); // ["read", "write"]示例 3:应用配置管理
使用字符串枚举定义配置选项,便于环境变量和配置文件管理:
typescript
enum Environment {
Development = "development",
Staging = "staging",
Production = "production"
}
enum LogLevel {
Debug = "debug",
Info = "info",
Warn = "warn",
Error = "error"
}
interface AppConfig {
environment: Environment;
logLevel: LogLevel;
port: number;
apiUrl: string;
}
class ConfigManager {
private config: AppConfig;
constructor() {
// 从环境变量读取,使用字符串枚举确保类型安全
const env = process.env.NODE_ENV as Environment || Environment.Development;
const logLevel = process.env.LOG_LEVEL as LogLevel || LogLevel.Info;
this.config = {
environment: this.validateEnvironment(env),
logLevel: this.validateLogLevel(logLevel),
port: parseInt(process.env.PORT || '3000', 10),
apiUrl: process.env.API_URL || 'http://localhost:3000'
};
}
private validateEnvironment(env: string): Environment {
if (Object.values(Environment).includes(env as Environment)) {
return env as Environment;
}
throw new Error(`Invalid environment: ${env}`);
}
private validateLogLevel(level: string): LogLevel {
if (Object.values(LogLevel).includes(level as LogLevel)) {
return level as LogLevel;
}
throw new Error(`Invalid log level: ${level}`);
}
getConfig(): AppConfig {
return { ...this.config };
}
isDevelopment(): boolean {
return this.config.environment === Environment.Development;
}
isProduction(): boolean {
return this.config.environment === Environment.Production;
}
shouldLog(level: LogLevel): boolean {
const levels = [LogLevel.Debug, LogLevel.Info, LogLevel.Warn, LogLevel.Error];
const currentIndex = levels.indexOf(this.config.logLevel);
const requestedIndex = levels.indexOf(level);
return requestedIndex >= currentIndex;
}
}
const configManager = new ConfigManager();
const config = configManager.getConfig();
console.log(config.environment); // "development"(从环境变量读取)
console.log(config.logLevel); // "info"
// 根据环境执行不同逻辑
if (configManager.isDevelopment()) {
console.log("开发模式:启用详细日志");
}
if (configManager.shouldLog(LogLevel.Debug)) {
console.log("Debug 日志已启用");
}示例 4:状态机实现
使用字符串枚举实现状态机,字符串值便于日志记录和调试:
typescript
enum OrderStatus {
Pending = "pending",
Processing = "processing",
Shipped = "shipped",
Delivered = "delivered",
Cancelled = "cancelled"
}
interface Order {
id: number;
status: OrderStatus;
items: string[];
}
class OrderManager {
private orders: Order[] = [];
// 定义允许的状态转换
private allowedTransitions: Record<OrderStatus, OrderStatus[]> = {
[OrderStatus.Pending]: [OrderStatus.Processing, OrderStatus.Cancelled],
[OrderStatus.Processing]: [OrderStatus.Shipped, OrderStatus.Cancelled],
[OrderStatus.Shipped]: [OrderStatus.Delivered],
[OrderStatus.Delivered]: [], // 最终状态,不能转换
[OrderStatus.Cancelled]: [] // 最终状态,不能转换
};
createOrder(items: string[]): Order {
const order: Order = {
id: Date.now(),
status: OrderStatus.Pending,
items
};
this.orders.push(order);
this.logStatusChange(order.id, null, order.status);
return order;
}
updateStatus(orderId: number, newStatus: OrderStatus): boolean {
const order = this.orders.find(o => o.id === orderId);
if (!order) {
console.error(`订单 ${orderId} 不存在`);
return false;
}
const oldStatus = order.status;
// 检查状态转换是否允许
if (!this.canTransitionTo(oldStatus, newStatus)) {
console.error(
`订单 ${orderId} 不能从 "${oldStatus}" 转换到 "${newStatus}"`
);
return false;
}
order.status = newStatus;
this.logStatusChange(orderId, oldStatus, newStatus);
return true;
}
private canTransitionTo(from: OrderStatus, to: OrderStatus): boolean {
return this.allowedTransitions[from]?.includes(to) ?? false;
}
private logStatusChange(
orderId: number,
oldStatus: OrderStatus | null,
newStatus: OrderStatus
): void {
if (oldStatus === null) {
console.log(`订单 ${orderId} 创建,状态: "${newStatus}"`);
} else {
console.log(
`订单 ${orderId} 状态变更: "${oldStatus}" → "${newStatus}"`
);
}
}
getOrderStatus(orderId: number): OrderStatus | undefined {
return this.orders.find(o => o.id === orderId)?.status;
}
getOrdersByStatus(status: OrderStatus): Order[] {
return this.orders.filter(o => o.status === status);
}
}
const manager = new OrderManager();
// 创建订单
const order1 = manager.createOrder(["商品A", "商品B"]);
// 订单 1234567890 创建,状态: "pending"
// 更新状态
manager.updateStatus(order1.id, OrderStatus.Processing);
// 订单 1234567890 状态变更: "pending" → "processing"
manager.updateStatus(order1.id, OrderStatus.Shipped);
// 订单 1234567890 状态变更: "processing" → "shipped"
manager.updateStatus(order1.id, OrderStatus.Delivered);
// 订单 1234567890 状态变更: "shipped" → "delivered"
// 尝试无效的状态转换
manager.updateStatus(order1.id, OrderStatus.Pending);
// 订单 1234567890 不能从 "delivered" 转换到 "pending"
// 查询特定状态的订单
const pendingOrders = manager.getOrdersByStatus(OrderStatus.Pending);
console.log(`待处理订单数: ${pendingOrders.length}`);示例 5:通过值获取名称(手动实现反向映射)
虽然字符串枚举不支持自动反向映射,但可以手动实现:
typescript
enum Theme {
Light = "light",
Dark = "dark",
Auto = "auto"
}
// 手动创建反向映射
const ThemeReverseMap: Record<string, keyof typeof Theme> = {
[Theme.Light]: "Light",
[Theme.Dark]: "Dark",
[Theme.Auto]: "Auto"
};
// 通过值获取名称
function getThemeName(value: Theme): string | undefined {
return ThemeReverseMap[value];
}
// 或者使用 Object.entries 动态创建
function createReverseMap<T extends Record<string, string>>(
enumObject: T
): Record<string, keyof T> {
const map: Record<string, keyof T> = {};
for (const [key, value] of Object.entries(enumObject)) {
map[value] = key as keyof T;
}
return map;
}
const themeReverseMap = createReverseMap(Theme);
console.log(themeReverseMap[Theme.Light]); // "Light"
console.log(themeReverseMap[Theme.Dark]); // "Dark"
console.log(themeReverseMap[Theme.Auto]); // "Auto"
// 使用示例
function setTheme(themeValue: string): void {
const themeName = themeReverseMap[themeValue];
if (themeName) {
console.log(`切换到主题: ${themeName} (${themeValue})`);
// 应用主题逻辑...
} else {
console.error(`无效的主题值: ${themeValue}`);
}
}
setTheme("light"); // "切换到主题: Light (light)"
setTheme("dark"); // "切换到主题: Dark (dark)"
setTheme("invalid"); // "无效的主题值: invalid"字符串枚举 vs 数字枚举
对比表格
| 特性 | 字符串枚举 | 数字枚举 |
|---|---|---|
| 成员值类型 | 字符串 | 数字 |
| 自动递增 | ❌ 不支持 | ✅ 支持 |
| 反向映射 | ❌ 不支持 | ✅ 支持 |
| 数值比较 | ❌ 不支持 | ✅ 支持 |
| 调试体验 | ✅ 显示有意义的字符串 | ⚠️ 显示数字 |
| 序列化 | ✅ 直接序列化为字符串 | ⚠️ 序列化为数字 |
| 代码体积 | 较小 | 较大(包含反向映射) |
| 类型安全 | ✅ 更严格 | ⚠️ 可能接受任意数字 |
| 与外部系统交互 | ✅ 适合(JSON、API) | ⚠️ 需要转换 |
选择建议
使用字符串枚举的场景:
API 交互:HTTP 方法、端点路径、请求类型等
typescriptenum HttpMethod { Get = "GET", Post = "POST" }配置管理:环境变量、配置键、选项值等
typescriptenum Environment { Development = "development", Production = "production" }权限系统:权限标识、角色名称等
typescriptenum Permission { Read = "read", Write = "write" }状态管理:需要序列化的状态值
typescriptenum OrderStatus { Pending = "pending", Completed = "completed" }需要更好的调试体验:日志中显示有意义的字符串而非数字
使用数字枚举的场景:
- 需要数值比较或范围判断:HTTP 状态码、优先级等
- 需要反向映射:通过值获取名称
- 值本身是数字:状态码、错误代码等
- 需要利用数值进行计算或排序
常见错误和注意事项
错误 1:忘记指定值
typescript
// ❌ 错误:字符串枚举必须为每个成员指定值
enum Color {
Red, // Error: Enum member must have initializer
Green = "green",
Blue = "blue"
}
// ✅ 正确:所有成员都指定值
enum Color {
Red = "red",
Green = "green",
Blue = "blue"
}错误 2:尝试使用反向映射
typescript
enum Direction {
Up = "UP",
Down = "DOWN"
}
// ❌ 错误:字符串枚举不支持反向映射
const name = Direction["UP"]; // undefined
// ✅ 正确:使用正向访问
const value = Direction.Up; // "UP"
// ✅ 正确:手动实现反向映射(如果需要)
const reverseMap: Record<string, string> = {
[Direction.Up]: "Up",
[Direction.Down]: "Down"
};
const name = reverseMap[Direction.Up]; // "Up"错误 3:类型检查不够严格
typescript
enum Status {
Active = "active",
Inactive = "inactive"
}
// ⚠️ 注意:类型断言可能绕过类型检查
function setStatus(status: Status): void {
// ...
}
// 虽然可以编译通过,但不推荐
setStatus("active" as Status);
// ✅ 正确:使用枚举成员
setStatus(Status.Active);注意事项
注意
- 必须显式指定值:字符串枚举的每个成员都必须显式指定字符串值,不能依赖自动递增
- 不支持反向映射:字符串枚举不支持通过值获取名称,需要手动实现
- 值的选择:选择有意义的字符串值,考虑序列化、日志记录和调试需求
- 类型检查:虽然字符串枚举类型检查更严格,但仍需注意类型断言的使用
提示
- 命名规范:枚举成员名使用 PascalCase,值使用有意义的字符串(通常小写或大写)
- 一致性:保持值的格式一致(如全部小写或全部大写)
- 序列化友好:字符串枚举值可以直接序列化为 JSON,无需转换
- 调试优势:在调试时,字符串枚举显示有意义的字符串,比数字更易读
- 与外部系统交互:字符串枚举特别适合与 REST API、配置文件等外部系统交互
重要
- 编译后体积:虽然字符串枚举比数字枚举体积小(无反向映射),但仍会增加代码体积
- 运行时存在:字符串枚举在编译后会生成 JavaScript 对象,如果只需要类型检查,考虑使用联合类型
- 值不可变:枚举成员的值在编译时确定,不能动态计算(除了引用其他枚举成员)
类型检查示例
类型安全的使用
typescript
enum LogLevel {
Debug = "debug",
Info = "info",
Warn = "warn",
Error = "error"
}
// ✅ 正确:使用枚举类型
function log(level: LogLevel, message: string): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
log(LogLevel.Info, "Application started");
log(LogLevel.Error, "Something went wrong");
// ❌ 错误:类型不匹配
log("info", "Message"); // Error: Argument of type '"info"' is not assignable to parameter of type 'LogLevel'
// ✅ 使用类型守卫确保值有效
function isValidLogLevel(value: string): value is LogLevel {
return Object.values(LogLevel).includes(value as LogLevel);
}
function logSafe(level: string, message: string): void {
if (isValidLogLevel(level)) {
console.log(`[${level.toUpperCase()}] ${message}`);
} else {
console.error(`Invalid log level: ${level}`);
}
}
logSafe("info", "Message"); // ✅ "[INFO] Message"
logSafe("invalid", "Message"); // ❌ "Invalid log level: invalid"枚举值的类型
typescript
enum Theme {
Light = "light",
Dark = "dark"
}
// 枚举类型
const theme: Theme = Theme.Light;
// 枚举成员的字面量类型
const lightTheme: Theme.Light = Theme.Light; // 更具体的类型
// 枚举值的类型是字符串字面量联合类型
type ThemeValue = Theme; // "light" | "dark"
// 在函数中使用
function setTheme(theme: Theme): void {
// theme 的类型是 Theme,值可能是 "light" 或 "dark"
console.log(`Setting theme to: ${theme}`);
}
setTheme(Theme.Light); // "Setting theme to: light"
setTheme(Theme.Dark); // "Setting theme to: dark"