Почему enum и radio-кнопки — идеальная пара?
Представьте: вы пишете форму на TypeScript. Нужно дать пользователю выбрать один вариант из нескольких — например, уровень сложности, статус заказа или пол. Где хранить эти варианты? Можно захардкодить массив строк, но это путь в никуда. Когда вариантов станет много, вы запутаетесь, а если поменяется бизнес-логика — придётся править код в десяти местах.
Тут на сцену выходят enum (перечисления). Они позволяют задать набор именованных констант, которые и читаются человеком, и понимаются машиной. А radio-кнопки — классический UI-элемент для выбора одного значения из списка. Соединить их — значит получить чистый, поддерживаемый код. Звучит круто, но есть нюанс: как именно перебрать enum, чтобы отобразить его в шаблоне? Именно этот вопрос мучает тысячи разработчиков на Stack Overflow. Давайте разберёмся раз и навсегда.
Проблема: enum — это не массив
В TypeScript enum — это не просто список строк. В зависимости от того, как вы его объявили (числовой или строковый), он ведёт себя по-разному. Числовой enum создаёт обратную связь: ключ -> значение и значение -> ключ. Строковый — только прямую. Поэтому просто взять и написать for (let item in MyEnum) — плохая идея. Вы получите не только нужные ключи, но и мусорные обратные отображения.
Вот как выглядит типичная ловушка:
enum Status {
Active = 1,
Inactive = 2,
Pending = 3
}
// Так делать НЕ надо
for (let key in Status) {
console.log(key); // "1", "2", "3", "Active", "Inactive", "Pending"
}
Видите? В консоль попали и названия, и значения. Если вы попытаетесь вывести это в radio-кнопки, пользователь увидит непонятные цифры. Нам это не подходит.
Решение №1: Object.values() для строковых enum
Самый простой и надёжный способ — использовать Object.values(). Этот метод возвращает массив значений перечисляемых свойств объекта. Для строковых enum он работает идеально, потому что у них нет обратного маппинга (почти всегда).
Пример: статус заказа
enum OrderStatus {
New = 'new',
Processing = 'processing',
Shipped = 'shipped',
Delivered = 'delivered',
Cancelled = 'cancelled'
}
// Получаем массив значений
const statusValues = Object.values(OrderStatus);
// ['new', 'processing', 'shipped', 'delivered', 'cancelled']
// Теперь можно легко отобразить в шаблоне (React)
function StatusSelector() {
return (
<div>
{statusValues.map((status) => (
<label key={status}>
<input type="radio" name="status" value={status} />
{status}
</label>
))}
</div>
);
}
Всё гениально просто. Мы получили массив строк и просто проитерировали его. Подходит для React, Angular, Vue — любого фреймворка.
Решение №2: Object.keys() + фильтрация для числовых enum
С числовыми enum ситуация хитрее. Object.values() вернёт не только строковые ключи, но и числовые значения, потому что JavaScript считает их «свойствами» объекта. Нам нужно отфильтровать только строки (ключи).
Пример: уровень доступа
enum AccessLevel {
Guest = 0,
User = 1,
Moderator = 2,
Admin = 3
}
// Фильтруем: оставляем только строки (ключи)
const levelKeys = Object.keys(AccessLevel).filter(key => isNaN(Number(key)));
// ['Guest', 'User', 'Moderator', 'Admin']
// Если нужно получить массив объектов для удобного отображения
const levelOptions = levelKeys.map(key => ({
label: key,
value: AccessLevel[key as keyof typeof AccessLevel]
}));
// [
// { label: 'Guest', value: 0 },
// { label: 'User', value: 1 },
// ...
// ]
Теперь у вас есть массив опций, где label — человекочитаемое имя, а value — числовой код, который удобно отправлять на сервер.
Решение №3: Кастомный хелпер (универсальный)
Если вам лень каждый раз писать фильтрацию, вынесите логику в функцию-хелпер. Это особенно удобно, если на проекте много enum разных типов.
// enum-utils.ts
type EnumType = Record<string, string | number>;
export function enumToOptions<T extends EnumType>(enumObj: T): { label: string; value: T[keyof T] }[] {
return (Object.keys(enumObj) as Array<keyof T>)
.filter(key => isNaN(Number(key))) // отбрасываем числовые ключи
.map(key => ({
label: String(key),
value: enumObj[key]
}));
}
// Использование
enum Color {
Red = '#FF0000',
Green = '#00FF00',
Blue = '#0000FF'
}
const colorOptions = enumToOptions(Color);
// [
// { label: 'Red', value: '#FF0000' },
// { label: 'Green', value: '#00FF00' },
// { label: 'Blue', value: '#0000FF' }
// ]
Этот хелпер работает и с числовыми, и со строковыми enum. Добавьте его в утилиты и забудьте о проблеме навсегда.
Решение №4: Использование Record и Map (продвинутый уровень)
Иногда enum используется не только для отображения, но и для дополнительных метаданных (описания, иконки, цвета). В таких случаях лучше создать отдельный маппинг через Record или Map.
enum TaskPriority {
Low = 'low',
Medium = 'medium',
High = 'high',
Critical = 'critical'
}
// Дополнительная информация для каждого приоритета
const priorityMeta: Record<TaskPriority, { label: string; color: string }> = {
[TaskPriority.Low]: { label: 'Низкий', color: 'green' },
[TaskPriority.Medium]: { label: 'Средний', color: 'orange' },
[TaskPriority.High]: { label: 'Высокий', color: 'red' },
[TaskPriority.Critical]: { label: 'Критический', color: 'darkred' }
};
// Теперь получаем массив для radio-кнопок
const priorityOptions = (Object.values(TaskPriority) as TaskPriority[]).map(value => ({
value,
...priorityMeta[value]
}));
// [
// { value: 'low', label: 'Низкий', color: 'green' },
// { value: 'medium', label: 'Средний', color: 'orange' },
// ...
// ]
// В шаблоне (React):
function PrioritySelector() {
return (
<div>
{priorityOptions.map(({ value, label, color }) => (
<label key={value} style={{ color }}>
<input type="radio" name="priority" value={value} />
{label}
</label>
))}
</div>
);
}
Этот подход — золотая середина между простотой и гибкостью. Вы отделяете данные