Вступление: Вечный вопрос на Stack Overflow
Вы когда-нибудь задумывались, как опытные разработчики на TypeScript получают имена значений enum? Если вы заглядывали на Stack Overflow, то наверняка видели вопрос с тысячами просмотров: "How to get names of enum entries?". Казалось бы, что тут сложного? Но на практике многие новички (и не только) спотыкаются на этом месте.
В этой статье мы не просто дадим готовый ответ. Мы залезем под капот TypeScript и разберем, как именно работают enum, как компилятор превращает их в JavaScript, и почему некоторые подходы к получению имен могут привести к неожиданным багам. Готовы? Поехали!
Что такое enum в TypeScript и зачем нужны их имена?
Enum (перечисление) — это удобный способ задать набор именованных констант. В TypeScript они бывают числовыми (по умолчанию) и строковыми. Но что делать, если вам нужно получить имя конкретного значения? Например, вы получили из API код статуса 403, а хотите вывести пользователю понятное сообщение "Forbidden".
enum HttpStatus {
OK = 200,
NotFound = 404,
Forbidden = 403
}
// Наша задача: получить имя "Forbidden" по значению 403
Кажется, что TypeScript должен предоставить встроенный метод для этого. Но нет. Приходится писать свою логику. И тут начинается самое интересное.
Решение "в лоб": Object.keys и Object.values
Самый очевидный способ — использовать методы Object.keys() или Object.values(). Многие новички пишут что-то вроде этого:
function getEnumName(enumObj: any, value: number): string | undefined {
const keys = Object.keys(enumObj);
return keys.find(key => enumObj[key] === value);
}
console.log(getEnumName(HttpStatus, 403)); // "Forbidden" — работает!
Выглядит логично? Да. Но если вы попробуете этот код на числовом enum, вас ждет сюрприз. Давайте посмотрим, во что компилируется наш HttpStatus.
Под капотом: компиляция числового enum
Когда TypeScript компилирует числовой enum, он создает объект с обратным отображением (reverse mapping). Вот как выглядит скомпилированный JavaScript:
var HttpStatus;
(function (HttpStatus) {
HttpStatus[HttpStatus["OK"] = 200] = "OK";
HttpStatus[HttpStatus["NotFound"] = 404] = "NotFound";
HttpStatus[HttpStatus["Forbidden"] = 403] = "Forbidden";
})(HttpStatus || (HttpStatus = {}));
// В итоге объект HttpStatus выглядит так:
// {
// "200": "OK",
// "404": "NotFound",
// "403": "Forbidden",
// "OK": 200,
// "NotFound": 404,
// "Forbidden": 403
// }
Видите фокус? Компилятор добавляет и ключи-числа, и ключи-строки. Поэтому Object.keys(HttpStatus) вернет не только ["OK", "NotFound", "Forbidden"], но и ["200", "404", "403"]. Это ломает логику нашего простого решения!
Чтобы обойти это, нужно отфильтровать числовые ключи:
function getEnumNameSafe(enumObj: any, value: number): string | undefined {
const keys = Object.keys(enumObj).filter(key => isNaN(Number(key)));
return keys.find(key => enumObj[key] === value);
}
Но такой код выглядит костыльно. Есть ли способ лучше?
Строковые enum: другая история
Если вы используете строковые enum, то reverse mapping не создается. Это упрощает задачу:
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
// Object.keys(Direction) вернет только ["Up", "Down", "Left", "Right"] — идеально!
Но что, если вам нужно получить имя по значению? Тот же самый Object.keys отлично работает:
function getStringEnumName(enumObj: any, value: string): string | undefined {
const keys = Object.keys(enumObj);
return keys.find(key => enumObj[key] === value);
}
console.log(getStringEnumName(Direction, "DOWN")); // "Down"
Мораль: если вам нужно часто получать имена enum значений, подумайте о переходе на строковые enum. Они более предсказуемы.
Продвинутый подход: TypeScript утилиты и дженерики
Чтобы не писать велосипеды каждый раз, можно создать универсальную утилиту с использованием дженериков. Это не только красиво, но и безопасно по типам:
type EnumObject = { [key: string]: number | string };
function getEnumName<T extends EnumObject>(
enumObj: T,
value: T[keyof T]
): keyof T | undefined {
const keys = Object.keys(enumObj) as Array<keyof T>;
return keys.find(key => enumObj[key] === value);
}
// Использование:
enum Status { Active = 1, Inactive = 0 }
const name = getEnumName(Status, 1); // TypeScript знает, что имя — "Active"
console.log(name); // "Active"
Обратите внимание: мы используем keyof T для возвращаемого типа. Это дает автодополнение и защиту от ошибок в IDE. Но есть нюанс: для числовых enum эта функция все равно будет возвращать и числа (ключи reverse mapping). Поэтому лучше использовать ее только со строковыми enum или добавить дополнительную фильтрацию.
Рефакторинг: Enum как const объект
Многие опытные разработчики в TypeScript сообществе (включая авторов популярных библиотек) рекомендуют избегать нативных enum в пользу const объектов с as const. Это дает больше контроля и предсказуемости:
const Colors = {
Red: '#FF0000',
Green: '#00FF00',
Blue: '#0000FF'
} as const;
type Colors = typeof Colors[keyof typeof Colors]; // "#FF0000" | "#00FF00" | "#0000FF"
function getColorName(colorValue: Colors): string | undefined {
const entry = Object.entries(Colors).find(([_, value]) => value === colorValue);
return entry ? entry[0] : undefined;
}
console.log(getColorName('#00FF00')); // "Green"
Преимущества такого подхода:
- Нет reverse mapping — объект остается "чистым".
- Полная типобезопасность — TypeScript выводит точные типы.
- Гибкость — можно легко добавить методы или метаданные прямо в объект.
- Производительность — меньше магии при компиляции.
Минус: вы теряете синтаксис enum и некоторые фичи (например, автоматическую инкрементацию). Но для большинства случаев это лучший выбор.
Типичные ошибки и как их избежать
Давайте разберем частые грабли, на которые наступают разработчики: