Avatar ·

Как получить имена (ключи) значений перечисления (enum) в TypeScript?

Привет! Я новичок в TypeScript и столкнулся с проблемой при работе с перечислениями (enums). Допустим, у меня есть такой enum: ```typescript enum Color { Red = 1, Green = 2, Blue = 4 } ``` Я хочу динамически получить список всех имен (ключей) этого перечисления, то есть массив строк: `["Red", "Green", "Blue"]`. Пытался использовать `Object.keys(Color)`, но в результате получаю не то, что ожидаю. В консоли появляются и числа: `["1", "2", "4", "Red", "Green", "Blue"]`. Почему так происходит? Это баг TypeScript? Мне нужно именно получить имена, чтобы, например, отобразить их в выпадающем списке на UI или для валидации вводимых пользователем значений. Как правильно это сделать? Есть ли встроенная функция или нужно писать свою? Буду благодарен за подробное объяснение с примерами. Также интересует, как быть с числовыми и строковыми перечислениями — есть ли разница в подходе? Заранее спасибо! <<>>

Почему Object.keys() возвращает числа?

Это не баг, а особенность работы TypeScript/JavaScript с числовыми перечислениями. При компиляции TypeScript генерирует так называемую обратную маппинг-таблицу (reverse mapping). Для каждого числового значения создается пара: ключ → значение и значение → ключ. Именно поэтому в массиве появляются как строковые ключи ("Red", "Green"), так и числовые ("1", "2").

Для строковых перечислений (где значения — строки) такой проблемы нет, так как обратная маппинг-таблица не создается. Но для числовых — это нужно учитывать.

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

Самый простой и надежный способ — отфильтровать только строковые ключи. Для этого можно использовать isNaN() или проверку на тип:

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}

// Способ 1: через isNaN
const colorNames = Object.keys(Color).filter(key => isNaN(Number(key)));
console.log(colorNames); // ["Red", "Green", "Blue"]

// Способ 2: через typeof (более явный)
const colorNames2 = Object.keys(Color).filter(key => typeof Color[key as any] === 'number');
console.log(colorNames2); // ["Red", "Green", "Blue"]

Универсальная функция для любых enum

Чтобы не писать фильтрацию каждый раз, создайте вспомогательную функцию:

function getEnumNames<T extends Record<string, string | number>>(enumObj: T): string[] {
  return Object.keys(enumObj).filter(key => {
    // Проверка: если значение — число, то это ключ, иначе — обратная маппинг-запись
    return isNaN(Number(key));
  });
}

// Пример использования:
enum Status {
  Active = 'active',
  Inactive = 'inactive',
  Pending = 0,
  Done = 1
}

console.log(getEnumNames(Color));  // ["Red", "Green", "Blue"]
console.log(getEnumNames(Status)); // ["Active", "Inactive", "Pending", "Done"]

Для строковых enum — всё проще

Если вы используете только строковые значения, можете просто брать Object.keys() без фильтрации:

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

console.log(Object.keys(Direction)); // ["Up", "Down", "Left", "Right"]

Альтернативный подход: через for...in

Можно также использовать цикл for...in, но он менее производителен и требует дополнительной проверки:

function getEnumNamesForIn(enumObj: any): string[] {
  const names: string[] = [];
  for (const key in enumObj) {
    if (isNaN(Number(key))) {
      names.push(key);
    }
  }
  return names;
}

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

  • const enum — компилируются в инлайн-значения и не имеют объекта во время выполнения. Для них Object.keys() не сработает вообще. Используйте обычные enum, если нужен доступ к именам.
  • Гетерогенные enum (смесь чисел и строк) — функция с фильтрацией isNaN будет работать корректно.
  • Производительность — фильтрация массива — операция O(n), но для перечислений с десятками элементов это не критично.

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

<<>>

Использование Object.values() и вспомогательного типа

Если вам нужно не просто получить имена, а, например, проверить, является ли строка допустимым значением enum, можно пойти от обратного — работать со значениями. Но для получения именно имен я предлагаю ещё один лаконичный способ с использованием TypeScript-утилит:

enum Fruit {
  Apple = 'apple',
  Banana = 'banana',
  Cherry = 5
}

// Получаем только строковые ключи (имена)
type EnumNames<T> = keyof T & string;
type FruitNames = EnumNames<typeof Fruit>; // "Apple" | "Banana" | "Cherry"

// Функция для безопасного получения имен
function getEnumKeys<T extends Record<string, string | number>>(enumObj: T): (keyof T)[] {
  return Object.keys(enumObj).filter(
    (key) => !enumObj[key as keyof T] || typeof enumObj[key as keyof T] === 'string' || typeof enumObj[key as keyof T] === 'number'
  ) as (keyof T)[];
}

const fruitNames = getEnumKeys(Fruit);
console.log(fruitNames); // ["Apple", "Banana", "Cherry"]

Когда это полезно

  • Если вы пишете библиотеку или утилиту, которая должна работать с любыми enum.
  • Если хотите сохранить типизацию — возвращаемый массив будет содержать именно имена ключей, а не просто строки.
  • Для случаев, когда enum может быть пустым или содержать только числовые значения — фильтрация всё равно сработает.

Этот подход чуть сложнее, но даёт больше контроля над типами. Для простых сценариев достаточно первого ответа с isNaN.