Avatar ·

Как преобразовать строку в enum в TypeScript?

Всем привет! Я новичок в TypeScript и столкнулся с проблемой при работе с перечислениями (enum). У меня есть enum для статусов заказа: ```typescript enum OrderStatus { Pending = "pending", Processing = "processing", Shipped = "shipped", Delivered = "delivered", Cancelled = "cancelled" } ``` Я получаю статус из API в виде строки, например `"processing"`. Мне нужно преобразовать эту строку обратно в значение enum, чтобы использовать его в switch/case или сравнивать с другими значениями. Простое приведение типа через `as OrderStatus` работает нестабильно — если строка не соответствует ни одному из значений, код не выдаёт ошибку компиляции, но в рантайме происходит непредсказуемое поведение. Также я пробовал использовать `OrderStatus["processing"]`, но это возвращает `undefined`. Как правильно и безопасно конвертировать строку в enum? Нужно решение, которое будет проверять, существует ли такое значение в enum, и выбрасывать ошибку или возвращать значение по умолчанию, если нет. Буду благодарен за примеры кода и объяснения! P.S. Я использую TypeScript 4.9+. <<>>

Правильное и безопасное преобразование строки в enum

Проблема, с которой вы столкнулись, очень распространена. Enum в TypeScript — это не просто набор констант, а объект, который имеет двустороннее отображение (для числовых enum) или одностороннее (для строковых). Рассмотрим несколько надёжных подходов.

1. Базовая проверка с помощью includes

Самый простой способ — проверить, является ли строка допустимым значением enum, используя Object.values():

enum OrderStatus {
  Pending = "pending",
  Processing = "processing",
  Shipped = "shipped",
  Delivered = "delivered",
  Cancelled = "cancelled"
}

function parseOrderStatus(value: string): OrderStatus {
  if (Object.values(OrderStatus).includes(value as OrderStatus)) {
    return value as OrderStatus;
  }
  throw new Error(`Invalid OrderStatus value: "${value}"`);
}

// Использование
const status = parseOrderStatus("processing"); // ✅ OrderStatus.Processing
const invalid = parseOrderStatus("unknown");   // ❌ Выбросит ошибку

Важно: Здесь value as OrderStatus используется только для удовлетворения TypeScript, реальная проверка происходит через includes.

2. Функция-утилита с дженериком

Для переиспользования создадим универсальную функцию:

function stringToEnum<T extends Record<string, string>>(
  enumObj: T,
  value: string
): T[keyof T] {
  const values = Object.values(enumObj) as string[];
  if (values.includes(value)) {
    return value as T[keyof T];
  }
  throw new Error(`Value "${value}" is not valid for enum`);
}

// Пример использования
const parsed = stringToEnum(OrderStatus, "shipped"); // ✅ OrderStatus.Shipped

3. Обработка undefined и значения по умолчанию

Если вы не хотите выбрасывать ошибку, можно возвращать значение по умолчанию:

function safeParseOrderStatus(value: string): OrderStatus | null {
  if (Object.values(OrderStatus).includes(value as OrderStatus)) {
    return value as OrderStatus;
  }
  return null; // или OrderStatus.Pending как fallback
}

// Или с дженериком:
function stringToEnumSafe<T extends Record<string, string>>(
  enumObj: T,
  value: string,
  defaultValue?: T[keyof T]
): T[keyof T] | undefined {
  const values = Object.values(enumObj) as string[];
  if (values.includes(value)) {
    return value as T[keyof T];
  }
  return defaultValue;
}

const status = safeParseOrderStatus("unknown") ?? OrderStatus.Pending;

4. Type Guard для проверки

Можно создать пользовательский type guard:

function isOrderStatus(value: string): value is OrderStatus {
  return Object.values(OrderStatus).includes(value as OrderStatus);
}

// Использование
const raw: string = getFromAPI();
if (isOrderStatus(raw)) {
  // TypeScript знает, что raw — это OrderStatus
  console.log(raw); // raw имеет тип OrderStatus
} else {
  // Обработка ошибки
}

5. Для числовых enum (дополнительно)

Числовые enum имеют обратное отображение, поэтому нужна дополнительная фильтрация:

enum NumericStatus {
  Active = 1,
  Inactive = 0,
  Pending = 2
}

function numericEnumFromString(enumObj: any, value: number): NumericStatus {
  // Отфильтровываем строковые ключи (обратное отображение)
  const numericValues = Object.values(enumObj).filter(v => typeof v === 'number');
  if (numericValues.includes(value)) {
    return value as NumericStatus;
  }
  throw new Error(`Invalid numeric value: ${value}`);
}

Рекомендации

  • Всегда проверяйте входные данные — не полагайтесь на as без валидации.
  • Используйте дженерики для создания переиспользуемых утилит.
  • Для больших проектов рассмотрите библиотеки вроде enum-utils или ts-enum-util.
  • В TypeScript 5.0+ можно использовать const-параметры для ещё более строгой типизации.

Надеюсь, эти примеры помогут вам безопасно работать с enum! Если остались вопросы — спрашивайте.

<<>>

Альтернативный подход: использование Map для lookup

Если вы работаете с большим количеством enum или часто выполняете преобразование, можно создать обратный lookup с помощью Map. Это будет работать быстрее, чем Object.values + includes (O(1) вместо O(n)):

enum OrderStatus {
  Pending = "pending",
  Processing = "processing",
  Shipped = "shipped"
}

// Создаём Map для быстрого поиска
const statusMap = new Map<string, OrderStatus>(
  Object.values(OrderStatus).map(value => [value, value])
);

function parseWithMap(value: string): OrderStatus {
  const result = statusMap.get(value);
  if (!result) {
    throw new Error(`Invalid status: "${value}"`);
  }
  return result;
}

// Использование
console.log(parseWithMap("shipped")); // OrderStatus.Shipped

Почему это может быть полезно?

  • Производительность: Map.has/get работают за O(1) в среднем.
  • Читаемость: Код явно показывает, что мы ищем значение в заготовленном словаре.
  • Безопасность: Map гарантирует, что ключи уникальны и соответствуют значениям enum.

Этот подход особенно удобен, если enum определён в отдельном модуле и вы не хотите каждый раз вызывать Object.values. Вы можете экспортировать Map вместе с enum для использования в других частях приложения.