Avatar ·

Как преобразовать TypeScript enum в массив объектов для отображения в UI?

Всем привет! Я новичок в TypeScript и столкнулся с проблемой при работе с enum. У меня есть перечисление статусов заказа: ```typescript enum OrderStatus { Pending = 'pending', Processing = 'processing', Shipped = 'shipped', Delivered = 'delivered', Cancelled = 'cancelled' } ``` Мне нужно отобразить эти статусы в выпадающем списке (select) в React-компоненте. Для этого мне нужен массив объектов вида: ```typescript [ { label: 'Pending', value: 'pending' }, { label: 'Processing', value: 'processing' }, // ... и так далее ] ``` Пробовал использовать `Object.values(OrderStatus)`, но это возвращает только значения ('pending', 'processing' и т.д.). Мне нужно получить и ключи (как label), и сами значения. Пробовал такой код: ```typescript const statusArray = Object.entries(OrderStatus).map(([key, value]) => ({ label: key, value: value })); ``` Но он почему-то возвращает массив с дублирующимися записями: `[{label: 'Pending', value: 'pending'}, {label: 'pending', value: 'Pending'}, ...]`. Я не понимаю, почему так происходит. Подскажите, как правильно преобразовать enum в массив объектов, где ключ enum становится label, а значение — value? Желательно универсальное решение, чтобы не писать каждый раз вручную. Заранее спасибо за помощь! <<>>

Проблема с дублированием в enum

Вы столкнулись с особенностью TypeScript enum. Когда enum содержит только строковые значения, TypeScript создает обратную маппинг-запись: для каждого ключа создается пара (ключ -> значение) и (значение -> ключ). Именно поэтому Object.entries() возвращает дублирующиеся записи.

Решение: фильтрация числовых ключей

Самый надежный способ — отфильтровать только строковые ключи, так как настоящие ключи enum всегда являются строками (не числами). Вот универсальное решение:

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

function enumToArray<T extends Record<string, string | number>>(enumObj: T): { label: string; value: T[keyof T] }[] {
  return Object.keys(enumObj)
    .filter(key => isNaN(Number(key))) // отсеиваем числовые ключи (обратная маппинг)
    .map(key => ({
      label: key,
      value: enumObj[key] as T[keyof T]
    }));
}

const statusArray = enumToArray(OrderStatus);
// Результат:
// [
//   { label: 'Pending', value: 'pending' },
//   { label: 'Processing', value: 'processing' },
//   { label: 'Shipped', value: 'shipped' },
//   { label: 'Delivered', value: 'delivered' },
//   { label: 'Cancelled', value: 'cancelled' }
// ]

Альтернативный подход: использование Object.values() с фильтрацией

Можно использовать комбинацию Object.values() и Object.keys():

function enumToArraySimple(enumObj: any): { label: string; value: string }[] {
  const keys = Object.keys(enumObj).filter(key => isNaN(Number(key)));
  return keys.map(key => ({
    label: key,
    value: enumObj[key]
  }));
}

Для числовых enum

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

enum NumericStatus {
  Pending = 0,
  Processing = 1,
  Shipped = 2
}

function numericEnumToArray(enumObj: any): { label: string; value: number }[] {
  const keys = Object.keys(enumObj).filter(key => isNaN(Number(key)));
  return keys.map(key => ({
    label: key,
    value: enumObj[key] as number
  }));
}

const numericArray = numericEnumToArray(NumericStatus);
// Результат:
// [
//   { label: 'Pending', value: 0 },
//   { label: 'Processing', value: 1 },
//   { label: 'Shipped', value: 2 }
// ]

Универсальный вариант с TypeScript generics

Вот полностью типизированное решение, которое работает с любыми enum:

type EnumType = Record<string, string | number>;

function enumToObjectArray<T extends EnumType>(
  enumObj: T
): Array<{ label: keyof T; value: T[keyof T] }> {
  return (Object.keys(enumObj) as Array<keyof T>)
    .filter(key => isNaN(Number(key)))
    .map(key => ({
      label: key,
      value: enumObj[key]
    }));
}

// Использование:
const result = enumToObjectArray(OrderStatus);
// TypeScript корректно выведет тип:
// Array<{ label: "Pending" | "Processing" | "Shipped" | "Delivered" | "Cancelled"; value: string }>

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

  • Всегда используйте isNaN(Number(key)) для фильтрации обратной маппинг-записей
  • Для строковых enum достаточно простой фильтрации
  • Для числовых enum проверка isNaN также работает корректно
  • Если вы используете const enum (с модификатором const), такой подход не сработает — const enum не имеют runtime-представления

Этот метод гарантированно избавит вас от дублирующихся записей и даст чистый массив объектов, готовый для использования в UI-компонентах.

<<>>

Простое решение через reduce

Можно использовать более лаконичный подход с методом reduce, который сразу фильтрует и преобразует enum:

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

const statusArray = Object.keys(OrderStatus)
  .reduce((acc, key) => {
    if (isNaN(Number(key))) {
      acc.push({ label: key, value: OrderStatus[key as keyof typeof OrderStatus] });
    }
    return acc;
  }, [] as { label: string; value: string }[]);

console.log(statusArray);
// Результат: правильный массив без дубликатов

Использование сторонней библиотеки

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

import { $enum } from 'ts-enum-util';

const statusArray = $enum(OrderStatus)
  .getKeys()
  .map(key => ({
    label: key,
    value: $enum(OrderStatus).getValue(key)
  }));

Этот подход особенно удобен, если вам нужно часто преобразовывать разные enum в массивы для select-компонентов. Библиотека также поддерживает строгую типизацию и работает с числовыми enum.