Skip to content

字符串枚举

概述

字符串枚举(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)⚠️ 需要转换

选择建议

使用字符串枚举的场景

  1. API 交互:HTTP 方法、端点路径、请求类型等

    typescript
    enum HttpMethod {
      Get = "GET",
      Post = "POST"
    }
  2. 配置管理:环境变量、配置键、选项值等

    typescript
    enum Environment {
      Development = "development",
      Production = "production"
    }
  3. 权限系统:权限标识、角色名称等

    typescript
    enum Permission {
      Read = "read",
      Write = "write"
    }
  4. 状态管理:需要序列化的状态值

    typescript
    enum OrderStatus {
      Pending = "pending",
      Completed = "completed"
    }
  5. 需要更好的调试体验:日志中显示有意义的字符串而非数字

使用数字枚举的场景

  1. 需要数值比较或范围判断:HTTP 状态码、优先级等
  2. 需要反向映射:通过值获取名称
  3. 值本身是数字:状态码、错误代码等
  4. 需要利用数值进行计算或排序

常见错误和注意事项

错误 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);

注意事项

注意

  1. 必须显式指定值:字符串枚举的每个成员都必须显式指定字符串值,不能依赖自动递增
  2. 不支持反向映射:字符串枚举不支持通过值获取名称,需要手动实现
  3. 值的选择:选择有意义的字符串值,考虑序列化、日志记录和调试需求
  4. 类型检查:虽然字符串枚举类型检查更严格,但仍需注意类型断言的使用

提示

  1. 命名规范:枚举成员名使用 PascalCase,值使用有意义的字符串(通常小写或大写)
  2. 一致性:保持值的格式一致(如全部小写或全部大写)
  3. 序列化友好:字符串枚举值可以直接序列化为 JSON,无需转换
  4. 调试优势:在调试时,字符串枚举显示有意义的字符串,比数字更易读
  5. 与外部系统交互:字符串枚举特别适合与 REST API、配置文件等外部系统交互

重要

  1. 编译后体积:虽然字符串枚举比数字枚举体积小(无反向映射),但仍会增加代码体积
  2. 运行时存在:字符串枚举在编译后会生成 JavaScript 对象,如果只需要类型检查,考虑使用联合类型
  3. 值不可变:枚举成员的值在编译时确定,不能动态计算(除了引用其他枚举成员)

类型检查示例

类型安全的使用

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"

相关链接

基于 VitePress 构建