Enum

Enum (перечисление) — именованный набор констант, объединённых общим смыслом: числовые, строковые и const enum.

Зачем нужно

  • Группировка связанных констант под одним именем
  • Автодополнение и проверка допустимых значений
  • Самодокументирующийся код — Status.Active понятнее чем 1
  • Защита от опечаток и недопустимых значений

Где используется

  • Статусы: OrderStatus.Pending, UserRole.Admin
  • Конфигурация: LogLevel.Error, Direction.Up
  • HTTP-коды, коды ошибок, режимы работы
  • API-контракты: фиксированный набор допустимых значений

Предпосылки

Числовые enum

По умолчанию enum — числовой, значения автоинкрементируются от 0:

enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}

let dir: Direction = Direction.Up;
console.log(dir); // 0

// Можно задать начальное значение
enum StatusCode {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  NotFound = 404,
  InternalError = 500,
}

console.log(StatusCode.OK); // 200

// Автоинкремент от заданного значения
enum Priority {
  Low = 1,
  Medium, // 2
  High,   // 3
}

Reverse Mapping (обратное отображение)

Числовые enum поддерживают обратный маппинг — можно получить имя по значению:

enum Color {
  Red,   // 0
  Green, // 1
  Blue,  // 2
}

console.log(Color.Red);   // 0
console.log(Color[0]);    // "Red" — обратный маппинг
console.log(Color[1]);    // "Green"

// Что генерируется в JS:
// var Color;
// (function (Color) {
//     Color[Color["Red"] = 0] = "Red";
//     Color[Color["Green"] = 1] = "Green";
//     Color[Color["Blue"] = 2] = "Blue";
// })(Color || (Color = {}));

Строковые enum

Каждому члену нужно задать строковое значение:

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

console.log(Direction.Up); // "UP"

// Строковые enum НЕ поддерживают reverse mapping
// Direction["UP"] — ошибка

// Полезно для API и сериализации
enum ApiEndpoint {
  Users = "/api/users",
  Posts = "/api/posts",
  Comments = "/api/comments",
}

fetch(ApiEndpoint.Users); // fetch("/api/users")

Гетерогенные enum (смешанные)

Технически возможно, но не рекомендуется:

// НЕ рекомендуется
enum Mixed {
  No = 0,
  Yes = "YES",
}

Const Enum

const enum полностью удаляется при компиляции — значения инлайнятся:

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

const dir = Direction.Up;
// Компилируется в: const dir = "UP";
// Никакого объекта Direction в рантайме!

Преимущества const enum

const enum StatusCode {
  OK = 200,
  NotFound = 404,
}

if (response.status === StatusCode.OK) {
  // Компилируется в: if (response.status === 200)
  // Нет overhead от объекта enum
}

Ограничения const enum

const enum Color {
  Red,
  Green,
  Blue,
}

// Нельзя использовать:
// Color[0]                    — нет reverse mapping
// Object.keys(Color)          — объекта нет в рантайме
// const c: Color = getColor — нельзя динамически

// Проблема с isolatedModules (Babel, esbuild):
// const enum не работает с isolatedModules: true
// Решение: обычный enum или union types

Enum как тип

enum Role {
  Admin = "ADMIN",
  User = "USER",
  Guest = "GUEST",
}

// Enum можно использовать как тип
function checkAccess(role: Role): boolean {
  return role === Role.Admin;
}

checkAccess(Role.Admin); // OK
checkAccess("ADMIN");    // Ошибка! Строка "ADMIN" !== Role.Admin

// Для числовых enum — осторожно!
enum NumericRole {
  Admin, // 0
  User,  // 1
}

function check(role: NumericRole): void {}
check(NumericRole.Admin); // OK
check(0);                 // OK — числа совместимы с числовым enum
check(999);               // OK! Это "дырка" — любое число принимается

Альтернативы Enum — Union Types

Во многих случаях union типы предпочтительнее enum:

// Вместо enum:
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// Union type:
type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT";

// Использование одинаковое:
function move(dir: Direction): void {}
move("UP");   // OK
move("DIAGONAL"); // Ошибка!

Сравнение Enum vs Union

Критерий Enum Union Type
Runtime-объект Да (кроме const enum) Нет
Tree-shaking Может не работать Работает
isolatedModules Проблемы с const enum Нет проблем
Reverse mapping Числовые — да Нет
Итерация по значениям Object.values(Enum) Нужен массив
Размер бандла Больше Нулевой
Рефакторинг Проще (одно место) Нужен поиск по коду

Объект-константа как альтернатива

// as const объект — лучшее из обоих миров
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

// Тип из значений объекта
type Direction = (typeof Direction)[keyof typeof Direction];
// "UP" | "DOWN" | "LEFT" | "RIGHT"

// Есть runtime-объект для итерации
Object.values(Direction); // ["UP", "DOWN", "LEFT", "RIGHT"]

// Нет проблем с isolatedModules
// Tree-shaking работает
// Типобезопасно

Паттерны использования

Битовые флаги

enum Permission {
  None = 0,
  Read = 1 << 0,    // 1
  Write = 1 << 1,   // 2
  Execute = 1 << 2, // 4
  All = Read | Write | Execute, // 7
}

function hasPermission(userPerms: number, required: Permission): boolean {
  return (userPerms & required) === required;
}

const userPerms = Permission.Read | Permission.Write; // 3
console.log(hasPermission(userPerms, Permission.Read));    // true
console.log(hasPermission(userPerms, Permission.Execute)); // false

Discriminated Union с Enum

enum EventType {
  Click = "CLICK",
  Hover = "HOVER",
  Scroll = "SCROLL",
}

type ClickEvent = { type: EventType.Click; x: number; y: number };
type HoverEvent = { type: EventType.Hover; element: string };
type ScrollEvent = { type: EventType.Scroll; offset: number };

type AppEvent = ClickEvent | HoverEvent | ScrollEvent;

function handleEvent(event: AppEvent): void {
  switch (event.type) {
    case EventType.Click:
      console.log(`Click at ${event.x}, ${event.y}`);
      break;
    case EventType.Hover:
      console.log(`Hover on ${event.element}`);
      break;
    case EventType.Scroll:
      console.log(`Scroll to ${event.offset}`);
      break;
  }
}

Частые ошибки

  1. Числовые enum принимают любое число — используйте строковые enum для безопасности
  2. const enum с isolatedModules: true — не работает в Babel/esbuild, используйте обычный enum
  3. Забыть присвоить значения строковому enum — автоинкремент не работает для строк
  4. Не знать про tree-shaking — обычные enum создают runtime-объект, который может не вытряхнуться
  5. Использовать enum для всего — для простых случаев union types проще и легче
// Ошибка: забыл значение для строкового enum
enum Bad {
  A = "A",
  B,  // Ошибка! Enum member must have initializer
}

// Правильно:
enum Good {
  A = "A",
  B = "B",
}

Практика

  1. Создайте числовой enum HttpStatus с основными кодами (200, 404, 500)
  2. Создайте строковой enum LogLevel (debug, info, warn, error)
  3. Перепишите enum через as const объект — сравните поведение
  4. Используйте битовые флаги для системы разрешений
  5. Создайте discriminated union с enum-дискриминатором

Связанные темы

Ресурсы