Avatar ·

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

Всем привет! Я только начал изучать TypeScript и столкнулся с проблемой при работе с перечислениями (enum). У меня есть enum, описывающий статусы заказа: ```typescript enum OrderStatus { Pending = 'pending', Processing = 'processing', Shipped = 'shipped', Delivered = 'delivered', Cancelled = 'cancelled' } ``` Я получаю строку из API (например, из JSON-ответа сервера), которая содержит значение статуса, например, `'shipped'`. Мне нужно преобразовать эту строку обратно в тип `OrderStatus`, чтобы использовать её в switch-case или передать в функцию, ожидающую этот enum. Проблема в том, что простое присваивание не работает: ```typescript const statusFromApi: string = 'shipped'; const orderStatus: OrderStatus = statusFromApi; // Ошибка: Type 'string' is not assignable to type 'OrderStatus' ``` Я пробовал использовать приведение типов (`as OrderStatus`), но это кажется небезопасным — если придёт строка, которой нет в enum, ошибка не возникнет, но поведение будет непредсказуемым. Как правильно и безопасно конвертировать строку в enum в TypeScript? Есть ли встроенные методы или лучшие практики для этого? Заранее спасибо за помощь! <<>>

Проблема приведения строки к enum

Действительно, прямое приведение через as OrderStatus — это обман компилятора, а не безопасное преобразование. TypeScript не предоставляет встроенного метода для конвертации строки в enum, но есть несколько надёжных подходов.

Решение 1: Функция-утилита с проверкой

Напишем функцию, которая принимает строку, проверяет её наличие в enum и возвращает корректное значение или выбрасывает ошибку:

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

function parseOrderStatus(value: string): OrderStatus {
  // Проверяем, есть ли значение в enum
  if (Object.values(OrderStatus).includes(value as OrderStatus)) {
    return value as OrderStatus;
  }
  throw new Error(`Invalid OrderStatus value: "${value}"`);
}

// Использование
const statusFromApi: string = 'shipped';
try {
  const orderStatus = parseOrderStatus(statusFromApi); // ✅ OrderStatus.Shipped
  console.log(orderStatus); // 'shipped'
} catch (error) {
  console.error(error.message);
}

Решение 2: Type guard + генерализация

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

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

// Использование
const status = stringToEnum(OrderStatus, 'processing'); // ✅ OrderStatus.Processing
const invalid = stringToEnum(OrderStatus, 'unknown');   // ❌ Error

Решение 3: Использование Map для обратного поиска

Если enum содержит много значений, можно создать Map для быстрого доступа:

class OrderStatusHelper {
  private static readonly statusMap: Map<string, OrderStatus> = new Map(
    Object.entries(OrderStatus)
  );

  static fromString(value: string): OrderStatus {
    const status = this.statusMap.get(value);
    if (!status) {
      throw new Error(`Unknown status: "${value}"`);
    }
    return status;
  }
}

const status = OrderStatusHelper.fromString('delivered'); // ✅ OrderStatus.Delivered

Решение 4: Безопасное приведение с возвратом значения по умолчанию

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

function safeParseOrderStatus(value: string): OrderStatus | null {
  if (Object.values(OrderStatus).includes(value as OrderStatus)) {
    return value as OrderStatus;
  }
  return null; // или значение по умолчанию
}

const status = safeParseOrderStatus('shipped') ?? OrderStatus.Pending;

Важные замечания

  • Числовые enum: для числовых enum (enum Status { Active = 1, Inactive = 2 }) нужно использовать Object.keys() вместо Object.values(), так как TypeScript создаёт двунаправленное отображение.
  • Const enum: const enum не существует в рантайме, поэтому все эти методы не сработают. Для const enum нужно использовать обычные объекты или union-типы.
  • Типобезопасность: Всегда проверяйте входные данные — это защитит от ошибок выполнения.

Лично я рекомендую второй вариант (универсальная функция) — он переиспользуем и хорошо читается. Для production-кода добавьте логирование ошибок и обработку в сервисном слое.

<<>>

Альтернативный подход: Использование union-типов вместо enum

Многие разработчики в современном TypeScript отказываются от enum в пользу union-типов (строковых литералов). Это решает проблему конвертации на корню:

type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

const orderStatuses = {
  Pending: 'pending' as const,
  Processing: 'processing' as const,
  Shipped: 'shipped' as const,
  Delivered: 'delivered' as const,
  Cancelled: 'cancelled' as const
};

// Теперь любая строка, прошедшая проверку, автоматически является OrderStatus
function parseOrderStatus(value: string): OrderStatus {
  const validStatuses: OrderStatus[] = ['pending', 'processing', 'shipped', 'delivered', 'cancelled'];
  if (validStatuses.includes(value as OrderStatus)) {
    return value as OrderStatus;
  }
  throw new Error(`Invalid status: "${value}"`);
}

// Использование
const status: OrderStatus = 'shipped'; // ✅ Без конвертации

Почему это может быть лучше?

  • Нет двойной структуры: не нужно хранить enum и маппинг отдельно.
  • Лучшая интеграция с JSON: строки из API уже являются корректным типом.
  • Tree-shaking: union-типы не генерируют JavaScript-код, в отличие от enum.
  • Простота: меньше boilerplate-кода для конвертации.

Этот подход особенно удобен, если вы работаете с REST API или GraphQL, где данные приходят в виде строк. Если вам нужны методы и свойства, ассоциированные со статусами, используйте объект-константу с функциями вместо enum.