Avatar ·

Как проверить, существует ли значение в enum в TypeScript?

Привет! Я новичок в TypeScript и столкнулся с проблемой при работе с перечислениями (enum). У меня есть вот такой enum: ```typescript enum UserRole { Admin = 'admin', User = 'user', Guest = 'guest' } ``` Мне нужно проверить, является ли строка, полученная из API (например, 'moderator'), допустимым значением этого enum. Я пробовал использовать `Object.values(UserRole).includes(value)`, но TypeScript ругается, что includes не работает с enum. Пробовал также `value in UserRole`, но это проверяет ключи, а не значения. В JavaScript я мог бы просто использовать `Object.values(...)`, но в TypeScript всё сложнее из-за типов. Подскажите, как правильно и типобезопасно проверить, что строка является одним из значений enum? Желательно с примером кода. <<>>

Проверка существования значения в enum в TypeScript

Проблема, с которой вы столкнулись, действительно типична. В TypeScript enum — это не просто набор констант, а специальная конструкция, которая создаёт как тип, так и объект. Для проверки существования значения нужно учитывать, работаете ли вы с числовыми или строковыми enum. Рассмотрим оба случая.

Для строковых enum (ваш случай):

enum UserRole {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest'
}

// Простая проверка через Object.values
function isUserRole(value: string): value is UserRole {
  return Object.values(UserRole).includes(value as UserRole);
}

// Использование
const testValue = 'moderator';
if (isUserRole(testValue)) {
  console.log('Допустимое значение');
} else {
  console.log('Недопустимое значение'); // выведет это
}

Обратите внимание на value is UserRole — это TypeScript-гард, который сужает тип. Однако as UserRole внутри функции — это костыль. Лучше использовать более строгий подход:

function isUserRole(value: string): value is UserRole {
  const values = Object.values(UserRole);
  return (values as string[]).includes(value);
}

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

enum Status {
  Active = 1,
  Inactive = 2,
  Pending = 3
}

function isStatus(value: number): value is Status {
  return Object.values(Status).includes(value);
}

// Но! Для числовых enum Object.values возвращает и ключи, и значения
// Поэтому лучше использовать фильтрацию:
function isStatusSafe(value: number): value is Status {
  return (Object.values(Status) as number[]).includes(value);
}

Универсальное решение с помощью дженериков:

function isEnumValue<T extends Record<string, string | number>>(
  enumObj: T,
  value: unknown
): value is T[keyof T] {
  const values = Object.values(enumObj);
  return values.includes(value as T[keyof T]);
}

// Использование:
if (isEnumValue(UserRole, 'admin')) {
  // value теперь имеет тип UserRole
}

Важные моменты:

  • Для строковых enum — Object.values() возвращает только значения, всё чисто.
  • Для числовых enum — Object.values() возвращает массив с дублированием (ключи и значения), поэтому нужна фильтрация.
  • TypeScript гарды (value is Type) помогают сохранять типобезопасность.
  • const enum не работают с Object.values() — они исчезают после компиляции.

Альтернатива с Map для производительности:

const userRoleValues = new Set(Object.values(UserRole));

function isUserRoleFast(value: string): value is UserRole {
  return userRoleValues.has(value as UserRole);
}

Этот подход быстрее для частых проверок, так как Set использует хэш-таблицу.

<<>>

Дополнительный подход: проверка через ключи enum

Иногда проще перевернуть задачу — проверять не значения, а ключи. Это особенно удобно, если ваши значения уникальны и вы хотите получить ключ по значению:

enum UserRole {
  Admin = 'admin',
  User = 'user',
  Guest = 'guest'
}

// Создаём обратное отображение
const roleByValue = Object.fromEntries(
  Object.entries(UserRole).map(([key, value]) => [value, key])
);

function getRoleKey(value: string): keyof typeof UserRole | undefined {
  return roleByValue[value] as keyof typeof UserRole | undefined;
}

// Использование
const key = getRoleKey('admin'); // 'Admin'
if (key) {
  console.log(`Роль: ${key}`);
}

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

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

Недостаток: этот подход требует дополнительной памяти для хранения обратного отображения, но для небольших enum это не критично.