$ sudo teach IT
МОДУЛЬ 3 · УРОК 5

Enum: перечисления

Числовые, строковые и const enum — как создавать именованные наборы значений, которые делают код понятнее и безопаснее

~25 минут Для новичков

1. Что такое перечисление — аналогия с пультом TV

Представьте пульт от телевизора. На нём есть кнопки: Power, Volume+, Volume-, Channel+, Channel-, Mute. Каждая кнопка имеет своё название и свою функцию. Вы не можете нажать кнопку, которой нет на пульте — пульт задаёт вам набор допустимых действий.

Перечисление (enum) в TypeScript — это точно такая же штука. Вы объявляете набор именованных значений, и далее в коде можете использовать только эти значения. Это называется исчерпывающим набором (exhaustive set) — ни больше, ни меньше.

Без enum вам пришлось бы работать со «голыми» строками или числами:

let direction = "north";  // Опечатка? "nort" случайно? TypeScript не знает
let role = "admin";       // А если кто-то напишет "admim"?
let status = 1;           // Что значит 1? Загадка...

Проблема очевидна — ничего не мешает допустить ошибку, и TypeScript не сможет её поймать. А вот с enum:

enum Direction {
  Up,
  Down,
  Left,
  Right
}

let dir: Direction = Direction.Up;  // Безопасно и понятно

Теперь переменная dir может принимать только одно из четырёх значений: Direction.Up, Direction.Down, Direction.Left или Direction.Right. Если вы попытаетесь присвоить что-то другое — TypeScript покажет ошибку.

Ключевые преимущества enum:

✅ Читаемость — вместо «магических чисел» используются осмысленные имена
✅ Безопасность — нельзя присвоить значение, которого нет в перечислении
✅ Автодополнение — IDE подсказывает все доступные варианты
✅ Рефакторинг — переименовали значение — обновилось везде
✅ Набор значений — можно перебрать все варианты

2. Числовые enum

Самый распространённый вид перечислений. Если вы не указываете значения явно, TypeScript начинает нумеровать элементы с нуля.

Простой числовой enum

enum Season {
  Spring,   // 0
  Summer,   // 1
  Autumn,   // 2
  Winter    // 3
}

console.log(Season.Spring);  // 0
console.log(Season.Summer);  // 1
console.log(Season.Autumn);  // 2
console.log(Season.Winter);  // 3

Обратите внимание: запятые после каждого значения — это стандарт. Запятая после последнего элемента (Winter) не обязательна, но и не запрещена.

Числовой enum с явными значениями

Вы можете задать конкретные числа вместо автоматической нумерации:

enum HttpStatus {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  ServerError = 500
}

console.log(HttpStatus.OK);           // 200
console.log(HttpStatus.NotFound);     // 404
console.log(HttpStatus.ServerError);  // 500

Теперь вместо магического числа 404 в коде будет HttpStatus.NotFound — сразу понятно, что это ошибка «не найдено».

Смешанное нумерование

Можно задать значение первому элементу, а остальные пойдут по порядку:

enum Status {
  Pending = 1,   // Явно задали 1
  Active,        // 2 (автоматически +1)
  Completed,     // 3
  Cancelled      // 4
}

console.log(Status.Pending);    // 1
console.log(Status.Active);     // 2
console.log(Status.Completed);  // 3
console.log(Status.Cancelled);  // 4

Совет: Когда вы задаёте значение первому элементу, TypeScript продолжает нумерацию от этого значения. Если задать Pending = 5, то следующие будут 6, 7, 8 и так далее.

Пример: роли пользователей

enum UserRole {
  Guest,      // 0
  User,       // 1
  Moderator,  // 2
  Admin,      // 3
  SuperAdmin  // 4
}

let currentRole: UserRole = UserRole.User;
console.log(currentRole);  // 1

Код стал читаемым. Вместо let currentRole = 1; мы видим UserRole.User — и сразу понимаем смысл.

3. Строковые enum

Строковые enum — это перечисления, где каждому элементу соответствует строковое значение вместо числа. Это удобно, когда:

  • Вы отправляете данные на сервер и хотите видеть осмысленные строки в JSON
  • Вы логируете значения и хотите видеть читаемый вывод
  • Вы отлаживаете код — строкилегче понять, чем числа

Простой строковый enum

enum Color {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
  Yellow = "YELLOW"
}

let myColor: Color = Color.Red;
console.log(myColor);        // "RED"
console.log(typeof myColor); // "string"

Обратите внимание: у строковых enum нет автоматической нумерации. Каждое значение нужно задавать явно — иначе TypeScript покажет ошибку.

Ошибка при пропуске значения

// ❌ Ошибка! Строковые enum требуют явных значений
enum Color {
  Red,      // Ошибка: Требуется инициализатор
  Green,
  Blue
}

// ✅ Правильно
enum Color {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE"
}

Строковый enum для API

enum OrderStatus {
  Pending = "pending",
  Processing = "processing",
  Shipped = "shipped",
  Delivered = "delivered",
  Cancelled = "cancelled"
}

function getStatusMessage(status: OrderStatus): string {
  switch (status) {
    case OrderStatus.Pending:
      return "Ваш заказ ожидает обработки";
    case OrderStatus.Processing:
      return "Ваш заказ обрабатывается";
    case OrderStatus.Shipped:
      return "Ваш заказ отправлен";
    case OrderStatus.Delivered:
      return "Ваш заказ доставлен";
    case OrderStatus.Cancelled:
      return "Ваш заказ отменён";
  }
}

let currentStatus = OrderStatus.Shipped;
console.log(getStatusMessage(currentStatus));
// "Ваш заказ отправлен"

Преимущество строковых enum: когда вы отправляете данные на сервер, в JSON будет не число 2, а строка "shipped" — сразу понятно и при отладке, и при чтении логов.

Строковый enum для дней недели

enum DayOfWeek {
  Monday = "Понедельник",
  Tuesday = "Вторник",
  Wednesday = "Среда",
  Thursday = "Четверг",
  Friday = "Пятница",
  Saturday = "Суббота",
  Sunday = "Воскресенье"
}

let today: DayOfWeek = DayOfWeek.Wednesday;
console.log(today);  // "Среда"

function isWeekend(day: DayOfWeek): boolean {
  return day === DayOfWeek.Saturday || day === DayOfWeek.Sunday;
}

console.log(isWeekend(DayOfWeek.Saturday));  // true
console.log(isWeekend(DayOfWeek.Monday));    // false

4. Смешанные enum

Технически вы можете комбинировать числа и строки в одном enum, но это не рекомендуется — так код становится запутанным. Однако знание о такой возможности полезно, чтобы понимать, что можно встретить в чужом коде:

// ⚠️ Не рекомендуется! Только для примера
enum MixedEnum {
  First = "first",
  Second = 2,
  Third = "third"
}

console.log(MixedEnum.First);   // "first"
console.log(MixedEnum.Second);  // 2
console.log(MixedEnum.Third);   // "third"

Важно: Смешанные enum создают путаницу. Всегда предпочитайте либо числовой, либо строковый enum. Если вам нужны и числа, и строки — создайте два отдельных enum или используйте объект-константу (об этом поговорим позже).

5. Const enum

const enum — это особый вид перечисления, которое полностью стирается при компиляции. TypeScript заменяет все обращения к const enum на соответствующие значения «на лету».

Обычный enum vs const enum

// Обычный enum — создаёт объект в JavaScript
enum Direction {
  Up,
  Down,
  Left,
  Right
}

// const enum — стирается при компиляции
const enum DirectionConst {
  Up,
  Down,
  Left,
  Right
}

Что происходит при компиляции

// Исходный код (TypeScript)
const enum ColorConst {
  Red,
  Green,
  Blue
}

let c = ColorConst.Red;

// Что получается после компиляции (JavaScript):
// let c = 0;  // enum полностью исчез!

Вот что TypeScript делает с обычным enum:

// Обычный enum после компиляции — объект остаётся!
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

Как видите, обычный enum создаёт целый JavaScript-объект. А const enum — нет. Это значит:

Преимущества const enum:
✅ Меньше кода в бандле — объект не создаётся
✅ Быстрее выполнение — обращение к числу быстрее, чем к свойству объекта
✅ Экономия места — нет JavaScript-кода для enum

Недостатки:
❌ Нет обратного маппинга (об этом ниже)
❌ Сложнее отладка — в консоли видно только число
❌ Проблемы с some библиотеками, которые ожидают обычный enum

Пример: битовые флаги

const enum особенно полезен для битовых флагов, где экономия места важна:

const enum Permission {
  Read = 1,      // 0001
  Write = 2,     // 0010
  Execute = 4,   // 0100
  Admin = 8      // 1000
}

let userPermission = Permission.Read | Permission.Write;
// userPermission = 1 | 2 = 3

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

console.log(hasPermission(userPermission, Permission.Read));    // true
console.log(hasPermission(userPermission, Permission.Execute)); // false
console.log(hasPermission(userPermission, Permission.Admin));   // false

Когда использовать const enum

Используйте const enum:
✅ Когда вам не нужен объект enum в runtime
✅ Когда критичен размер бандла (мобильные приложения, embedded)
✅ Когда не нужен reverse mapping
✅ Для битовых флагов и констант

Используйте обычный enum:
✅ Когда нужен reverse mapping
✅ Когда enum передаётся как значение во внешние библиотеки
✅ Когда нужно отлаживать и видеть осмысленные имена

6. Автоматические vs явные значения

Давайте разберём подробнее, как TypeScript присваивает значения элементам enum.

Автоматическая нумерация

enum Numbers {
  One,    // 0 (начинаем с нуля)
  Two,    // 1
  Three,  // 2
  Four,   // 3
  Five    // 4
}

// Простое правило: каждый следующий элемент = предыдущий + 1

Явные значения — полный контроль

enum Priority {
  Low = 1,       // Явно задали 1
  Medium = 5,    // Явно задали 5
  High = 10,     // Явно задали 10
  Critical = 100 // Явно задали 100
}

// Значения не связаны между собой — каждый задан вручную

Комбинация: первый элемент задан, остальные автоматические

enum ErrorCode {
  NotFound = 4000,    // Явно: 4000
  BadRequest,         // Авто: 4001
  Unauthorized,       // Авто: 4002
  Forbidden,          // Авто: 4003
  ServerError = 5000, // Явно: 5000
  Timeout,            // Авто: 5001
  Internal            // Авто: 5002
}

console.log(ErrorCode.NotFound);      // 4000
console.log(ErrorCode.Unauthorized);  // 4002
console.log(ErrorCode.ServerError);   // 5000
console.log(ErrorCode.Internal);      // 5002

Как только вы задали новое явное значение, счётчик сбрасывается и продолжает от него.

Опасные повторения значений

// ⚠️ TypeScript РАЗРЕШАЕТ это, но это ОПАСНО
enum BadEnum {
  A = 1,
  B = 2,
  C = 1  // Тот же номер, что и A!
}

// A и C оба равны 1 — это источник багов
console.log(BadEnum.A === BadEnum.C);  // true 😱

Осторожно! TypeScript не запрещает дублирования числовых значений в enum. Это может привести к серьёзным багам. Всегда проверяйте, что ваши числовые значения уникальны. Для строковых enumэтот проблема не существует — там каждое значение уникально по определению.

7. Доступ к значениям enum

Давайте разберём все способы получить значения из enum.

Прямой доступ по имени

enum TrafficLight {
  Red,
  Yellow,
  Green
}

// Самый распространённый способ
let currentLight = TrafficLight.Red;
console.log(currentLight);  // 0

// Через точку — TypeScript покажет автодополнение!
let nextLight = TrafficLight.Yellow;
console.log(nextLight);  // 1

Доступ через квадратные скобки

enum Fruit {
  Apple = "apple",
  Banana = "banana",
  Cherry = "cherry"
}

// Доступ через точку (обычный способ)
let fruit1 = Fruit.Apple;

// Доступ через квадратные скобки
let key = "Banana";
let fruit2 = Fruit[key];  // "banana"

// Оба варианта дают одно и то же
console.log(fruit1);  // "apple"
console.log(fruit2);  // "banana"

Совет: Доступ через квадратные скобки полезен, когда имя элемента хранится в переменной. Например: Fruit[fruitName], где fruitName — строка, полученная от пользователя.

Получение всех значений enum

enum Role {
  Guest = "guest",
  User = "user",
  Admin = "admin"
}

// Получаем все строковые значения
let roleValues = Object.values(Role);
console.log(roleValues);  // ["guest", "user", "admin"]

// Получаем все имена
let roleNames = Object.keys(Role);
console.log(roleNames);  // ["Guest", "User", "Admin"]

// Получаем все пары "имя" -> "значение"
let roleEntries = Object.entries(Role);
console.log(roleEntries);
// [["Guest", "guest"], ["User", "user"], ["Admin", "admin"]]

Проверка вхождения значения в enum

enum Season {
  Spring,
  Summer,
  Autumn,
  Winter
}

function isValidSeason(value: unknown): value is Season {
  return Object.values(Season).includes(value as Season);
}

// Проверка
let input: unknown = 1;
if (isValidSeason(input)) {
  // Здесь input имеет тип Season
  console.log("Это сезон:", Season[input]);  // "Это сезон: Summer"
} else {
  console.log("Неверное значение");
}

8. Reverse mapping

Reverse mapping — это возможность получить имя элемента по его числовому значению. Это работает только с числовыми enum!

Как работает reverse mapping

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

// Прямой маппинг: имя -> число
console.log(Direction.Up);      // 0
console.log(Direction.Down);    // 1

// Обратный маппинг: число -> имя (строка)
console.log(Direction[0]);      // "Up"
console.log(Direction[1]);      // "Down"
console.log(Direction[2]);      // "Left"
console.log(Direction[3]);      // "Right"

Практический пример: конвертация

enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500
}

// Преобразование числа в читаемое имя
function getStatusName(code: number): string {
  return HttpStatus[code] || "Unknown";
}

console.log(getStatusName(200));  // "OK"
console.log(getStatusName(404));  // "NotFound"
console.log(getStatusName(500));  // "ServerError"
console.log(getStatusName(999));  // "Unknown"

Пример: логирование

enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3
}

function log(level: LogLevel, message: string): void {
  const levelName = LogLevel[level];
  console.log(`[${levelName}] ${message}`);
}

log(LogLevel.Debug, "Отладочное сообщение");
// [Debug] Отладочное сообщение

log(LogLevel.Error, "Критическая ошибка!");
// [Error] Критическая ошибка!

Почему reverse mapping не работает со строковыми enum

enum StringEnum {
  A = "alpha",
  B = "beta"
}

// Прямой маппинг работает
console.log(StringEnum.A);  // "alpha"

// ❌ Обратный маппинг НЕ работает!
console.log(StringEnum["alpha"]);  // undefined 😱

Важно: Reverse mapping работает только для числовых enum. Строковые enum не имеют обратного маппинга, потому что JavaScript-объект создается так, что свойства хранятся только в одну сторону (имя → значение).

Реальный пример: маппинг HTTP-методов

enum HttpMethod {
  GET = 0,
  POST = 1,
  PUT = 2,
  DELETE = 3,
  PATCH = 4
}

// Функция для отправки запроса
function sendRequest(method: HttpMethod, url: string): void {
  const methodName = HttpMethod[method];  // Reverse mapping!
  console.log(`Отправляем ${methodName} запрос на ${url}`);
}

sendRequest(HttpMethod.GET, "/api/users");
// Отправляем GET запрос на /api/users

sendRequest(HttpMethod.POST, "/api/users");
// Отправляем POST запрос на /api/users

sendRequest(HttpMethod.DELETE, "/api/users/123");
// Отправляем DELETE запрос на /api/users/123

9. Использование enum в функциях

Enum идеально подходят для параметров функций, когда набор допустимых значений ограничен.

Функция с enum-параметром

enum Animal {
  Dog = "dog",
  Cat = "cat",
  Bird = "bird",
  Fish = "fish"
}

function makeSound(animal: Animal): string {
  switch (animal) {
    case Animal.Dog:
      return "Гав!";
    case Animal.Cat:
      return "Мяу!";
    case Animal.Bird:
      return "Чик-чирик!";
    case Animal.Fish:
      return "...";
  }
}

console.log(makeSound(Animal.Dog));   // "Гав!"
console.log(makeSound(Animal.Cat));   // "Мяу!"
console.log(makeSound(Animal.Bird));  // "Чик-чирик!"

Enum как возвращаемый тип

enum Temperature {
  Freezing = "freezing",
  Cold = "cold",
  Warm = "warm",
  Hot = "hot"
}

function getTemperatureCategory(celsius: number): Temperature {
  if (celsius <= 0) return Temperature.Freezing;
  if (celsius <= 15) return Temperature.Cold;
  if (celsius <= 30) return Temperature.Warm;
  return Temperature.Hot;
}

let temp = getTemperatureCategory(25);
console.log(temp);  // "warm"

Enum по умолчанию (default value)

enum Theme {
  Light = "light",
  Dark = "dark",
  Auto = "auto"
}

function setTheme(theme: Theme = Theme.Auto): void {
  console.log("Тема установлена:", theme);
}

setTheme();              // Тема установлена: auto
setTheme(Theme.Light);   // Тема установлена: light
setTheme(Theme.Dark);    // Тема установлена: dark

Пример: система уведомлений

enum NotificationType {
  Info = "info",
  Success = "success",
  Warning = "warning",
  Error = "error"
}

interface Notification {
  type: NotificationType;
  title: string;
  message: string;
  timestamp: Date;
}

function createNotification(
  type: NotificationType,
  title: string,
  message: string
): Notification {
  return {
    type,
    title,
    message,
    timestamp: new Date()
  };
}

function displayNotification(notification: Notification): void {
  const emoji =
    notification.type === NotificationType.Success ? "✅" :
    notification.type === NotificationType.Error ? "❌" :
    notification.type === NotificationType.Warning ? "⚠️" :
    "ℹ️";

  console.log(`${emoji} [${notification.type.toUpperCase()}] ${notification.title}`);
  console.log(`   ${notification.message}`);
}

let notif = createNotification(
  NotificationType.Success,
  "Операция выполнена",
  "Ваши данные успешно сохранены"
);

displayNotification(notif);
// ✅ [SUCCESS] Операция выполнена
//    Ваши данные успешно сохранены

Пример: фильтрация данных

enum ProductCategory {
  Electronics = "electronics",
  Clothing = "clothing",
  Food = "food",
  Books = "books"
}

interface Product {
  name: string;
  category: ProductCategory;
  price: number;
}

let products: Product[] = [
  { name: "Телефон", category: ProductCategory.Electronics, price: 50000 },
  { name: "Рубашка", category: ProductCategory.Clothing, price: 3000 },
  { name: "Яблоки", category: ProductCategory.Food, price: 200 },
  { name: "Ноутбук", category: ProductCategory.Electronics, price: 80000 },
  { name: "Книга", category: ProductCategory.Books, price: 500 },
  { name: "Кофе", category: ProductCategory.Food, price: 400 }
];

function filterByCategory(
  items: Product[],
  category: ProductCategory
): Product[] {
  return items.filter(item => item.category === category);
}

let electronics = filterByCategory(products, ProductCategory.Electronics);
console.log("Электроника:", electronics.map(p => p.name));
// Электроника: ["Телефон", "Ноутбук"]

let food = filterByCategory(products, ProductCategory.Food);
console.log("Еда:", food.map(p => p.name));
// Еда: ["Яблоки", "Кофе"]

10. Enum vs union типы vs const объекты

В TypeScript есть три способа создать набор именованных значений. Давайте сравним их.

Вариант 1: Enum

enum Direction {
  Up,
  Down,
  Left,
  Right
}

let dir: Direction = Direction.Up;

Вариант 2: Union тип

type Direction = "up" | "down" | "left" | "right";

let dir: Direction = "up";

Вариант 3: Const объект

const Direction = {
  Up: "up",
  Down: "down",
  Left: "left",
  Right: "right"
} as const;

type Direction = typeof Direction[keyof typeof Direction];
let dir: Direction = Direction.Up;

Сравнительная таблица

┌─────────────────────┬──────────────────┬──────────────────┬──────────────────┐
│ Критерий            │ Enum             │ Union тип        │ Const объект     │
├─────────────────────┼──────────────────┼──────────────────┼──────────────────┤
│ Автодополнение      │ ✅ Да            │ ✅ Да            │ ✅ Да            │
│ Reverse mapping     │ ✅ Числовые      │ ❌ Нет           │ ❌ Нет           │
│ Runtime значение    │ ✅ Есть          │ ❌ Нет           │ ✅ Есть          │
│ Размер бандла       │ ⚠️ Большой       │ ✅ Нулевой       │ ✅ Маленький     │
│ Именование типов    │ ✅ Авто          │ ✅ Явное         │ ⚠️ Сложный синтакс│
│ Логирование         │ ⚠️ Числа/строки │ ❌ Неудобно      │ ✅ Строки        │
│ Производительность  │ ⚠️ Хуже          │ ✅ Лучше         │ ✅ Лучше         │
└─────────────────────┴──────────────────┴──────────────────┴──────────────────┘

Когда что использовать

Используйте enum:
• Когда нужен reverse mapping (число → имя)
• Когда enum передаётся в код на чистом JavaScript
• Когда нужен runtime объект для перебора значений

Используйте union типы:
• Для простых строковых наборов значений
• Когда не нужен runtime объект
• Когда критичен размер бандла

Используйте const объекты:
• Когда нужен и тип, и runtime значение
• Когда не нужен reverse mapping
• Когда хотите контролировать точные строковые значения

Практический пример: все три подхода

// Подход 1: Enum
enum StatusEnum {
  Active = "active",
  Inactive = "inactive",
  Banned = "banned"
}

// Подход 2: Union тип
type StatusUnion = "active" | "inactive" | "banned";

// Подход 3: Const объект
const StatusConst = {
  Active: "active",
  Inactive: "inactive",
  Banned: "banned"
} as const;

type StatusConstType = typeof StatusConst[keyof typeof StatusConst];

// Все три работают одинаково для типизации:
function getUserStatusEnum(status: StatusEnum): string {
  return `Статус: ${status}`;
}

function getUserStatusUnion(status: StatusUnion): string {
  return `Статус: ${status}`;
}

function getUserStatusConst(status: StatusConstType): string {
  return `Статус: ${status}`;
}

11. Ошибки новичков

Рассмотрим самые частые ошибки, которые допускают начинающие при работе с enum.

Ошибка 1: Попытка присвоить строку числовому enum

enum Direction {
  Up,
  Down,
  Left,
  Right
}

// ❌ Ошибка: строка не присваивается числовому enum
let dir: Direction = "Up";

// ✅ Правильно:
let dir2: Direction = Direction.Up;
let dir3: Direction = 0;  // Сработает, но не рекомендуется!

Важно: Числовой enum в TypeScript — это просто число. Вы можете написать let dir: Direction = 0 и это сработает. Но так нельзя делать — теряется вся выгода enum!

Ошибка 2: Забыли ключевое слово enum

// ❌ Ошибка: забыли enum
Direction {
  Up,
  Down
}

// ✅ Правильно:
enum Direction {
  Up,
  Down
}

Ошибка 3: Двоеточие вместо равно

// ❌ Ошибка: двоеточие вместо равно
enum Color {
  Red: "red",     // Так нельзя!
  Blue: "blue"
}

// ✅ Правильно: равно
enum Color {
  Red = "red",
  Blue = "blue"
}

Ошибка 4: Строковый enum без значений

// ❌ Ошибка: строковый enum требует явных значений
enum Fruit {
  Apple,    // Ошибка: Требуется инициализатор
  Banana,
  Cherry
}

// ✅ Правильно:
enum Fruit {
  Apple = "apple",
  Banana = "banana",
  Cherry = "cherry"
}

Ошибка 5: Запятая в конце (не ошибка, но путает)

// Это допустимо, но не рекомендуется
enum Color {
  Red = "red",
  Blue = "blue",   // Запятая после последнего элемента
}

// Лучше без запятой после последнего:
enum Color {
  Red = "red",
  Blue = "blue"
}

Ошибка 6: Думают, что enum — это объект

enum Status {
  Active = "active",
  Inactive = "inactive"
}

// ❌ Ошибка: enum не является обычным объектом
for (let key in Status) {
  console.log(key);  // Выведет и ключи, и значения!
}

// ✅ Правильно: используйте Object.values или Object.keys
let values = Object.values(Status);
console.log(values);  // ["active", "inactive"]

Ошибка 7: Сравнение enum с неверным типом

enum Color {
  Red = "red",
  Blue = "blue"
}

let userColor: string = "red";

// ❌ Ошибка: нельзя сравнивать string с Color
if (userColor === Color.Red) {  // Ошибка компиляции
  console.log("Красный!");
}

// ✅ Правильно: проверяем вхождение в enum
if (Object.values(Color).includes(userColor as Color)) {
  console.log("Это допустимый цвет!");
}

Ошибка 8: Enum в switch без exhaustiveness check

enum Shape {
  Circle,
  Rectangle,
  Triangle
}

// ⚠️ Работает, но небезопасно — забыли Triangle!
function getArea(shape: Shape): number {
  switch (shape) {
    case Shape.Circle:
      return 3.14;
    case Shape.Rectangle:
      return 4;
    // Triangle забыли! Нет ошибки компиляции
  }
}

// ✅ Правильно: добавьте default с проверкой
function getAreaSafe(shape: Shape): number {
  switch (shape) {
    case Shape.Circle:
      return 3.14;
    case Shape.Rectangle:
      return 4;
    case Shape.Triangle:
      return 2;
    default:
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

Совет для новичков: Используйте never в default-ветке switch по enum. Если вы забудете обработать какое-то значение — TypeScript покажет ошибку компиляции. Это называется exhaustiveness check (проверка полноты обработки).

12. Практические примеры

Давайте рассмотрим реальные ситуации, где enum незаменимы.

Пример 1: Состояние формы

enum FormState {
  Idle = "idle",
  Loading = "loading",
  Success = "success",
  Error = "error"
}

class LoginForm {
  state: FormState = FormState.Idle;
  errorMessage: string = "";

  async submit(username: string, password: string): Promise<void> {
    this.state = FormState.Loading;

    try {
      const response = await fetch("/api/login", {
        method: "POST",
        body: JSON.stringify({ username, password })
      });

      if (response.ok) {
        this.state = FormState.Success;
      } else {
        this.errorMessage = "Неверный логин или пароль";
        this.state = FormState.Error;
      }
    } catch (e) {
      this.errorMessage = "Ошибка сети";
      this.state = FormState.Error;
    }
  }

  getStatusMessage(): string {
    switch (this.state) {
      case FormState.Idle:
        return "Введите данные для входа";
      case FormState.Loading:
        return "Выполняется вход...";
      case FormState.Success:
        return "Добро пожаловать!";
      case FormState.Error:
        return `Ошибка: ${this.errorMessage}`;
    }
  }
}

let form = new LoginForm();
console.log(form.getStatusMessage());  // "Введите данные для входа"

Пример 2: Калькулятор с операциями

enum Operation {
  Add = "+",
  Subtract = "-",
  Multiply = "*",
  Divide = "/"
}

function calculate(
  a: number,
  b: number,
  operation: Operation
): number {
  switch (operation) {
    case Operation.Add:
      return a + b;
    case Operation.Subtract:
      return a - b;
    case Operation.Multiply:
      return a * b;
    case Operation.Divide:
      if (b === 0) throw new Error("Деление на ноль!");
      return a / b;
  }
}

console.log(calculate(10, 5, Operation.Add));       // 15
console.log(calculate(10, 5, Operation.Subtract));   // 5
console.log(calculate(10, 5, Operation.Multiply));   // 50
console.log(calculate(10, 5, Operation.Divide));     // 2

Пример 3: Конфигурация приложения

enum Environment {
  Development = "development",
  Staging = "staging",
  Production = "production"
}

enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3
}

interface AppConfig {
  env: Environment;
  logLevel: LogLevel;
  apiUrl: string;
  enableCache: boolean;
}

function createConfig(env: Environment): AppConfig {
  switch (env) {
    case Environment.Development:
      return {
        env,
        logLevel: LogLevel.Debug,
        apiUrl: "http://localhost:3000",
        enableCache: false
      };
    case Environment.Staging:
      return {
        env,
        logLevel: LogLevel.Info,
        apiUrl: "https://staging.api.example.com",
        enableCache: true
      };
    case Environment.Production:
      return {
        env,
        logLevel: LogLevel.Error,
        apiUrl: "https://api.example.com",
        enableCache: true
      };
  }
}

let config = createConfig(Environment.Production);
console.log(config);
// { env: "production", logLevel: 3, apiUrl: "https://api.example.com", enableCache: true }

Пример 4: Игровая механика

enum WeaponType {
  Sword = "sword",
  Bow = "bow",
  Staff = "staff",
  Dagger = "dagger"
}

enum DamageType {
  Physical = "physical",
  Magical = "magical",
  Poison = "poison"
}

interface Weapon {
  name: string;
  type: WeaponType;
  damageType: DamageType;
  baseDamage: number;
  criticalChance: number;
}

let weapons: Weapon[] = [
  { name: "Железный меч", type: WeaponType.Sword, damageType: DamageType.Physical, baseDamage: 15, criticalChance: 0.1 },
  { name: "Лук охотника", type: WeaponType.Bow, damageType: DamageType.Physical, baseDamage: 12, criticalChance: 0.15 },
  { name: "Посох мага", type: WeaponType.Staff, damageType: DamageType.Magical, baseDamage: 20, criticalChance: 0.05 },
  { name: "Кинжал теней", type: WeaponType.Dagger, damageType: DamageType.Physical, baseDamage: 10, criticalChance: 0.25 }
];

function calculateDamage(weapon: Weapon): number {
  const isCritical = Math.random() < weapon.criticalChance;
  const multiplier = isCritical ? 2 : 1;
  const damage = weapon.baseDamage * multiplier;

  console.log(
    `${weapon.name} наносит ${damage} урона` +
    (isCritical ? " (КРИТИЧЕСКИЙ УДАР!)" : "")
  );

  return damage;
}

calculateDamage(weapons[0]);  // Железный меч наносит 15 урона
calculateDamage(weapons[2]);  // Посох мага наносит 20 урона

Пример 5: Роутинг

enum Route {
  Home = "/",
  About = "/about",
  Contact = "/contact",
  Profile = "/profile",
  Settings = "/settings",
  Admin = "/admin"
}

function navigateTo(route: Route): void {
  console.log(`Навигация на: ${route}`);
  // В реальном приложении: window.history.pushState({}, "", route);
}

navigateTo(Route.Home);       // Навигация на: /
navigateTo(Route.Profile);    // Навигация на: /profile
navigateTo(Route.Settings);   // Навигация на: /settings

Пример 6: Валидация данных

enum ValidationRule {
  Required = "required",
  MinLength = "minLength",
  MaxLength = "maxLength",
  Email = "email",
  Number = "number",
  Pattern = "pattern"
}

interface Validation {
  rule: ValidationRule;
  value?: number | string;
  message: string;
}

function validate(
  input: string,
  validations: Validation[]
): string[] {
  const errors: string[] = [];

  for (const validation of validations) {
    switch (validation.rule) {
      case ValidationRule.Required:
        if (!input.trim()) {
          errors.push(validation.message);
        }
        break;
      case ValidationRule.MinLength:
        if (input.length < (validation.value as number)) {
          errors.push(validation.message);
        }
        break;
      case ValidationRule.MaxLength:
        if (input.length > (validation.value as number)) {
          errors.push(validation.message);
        }
        break;
      case ValidationRule.Email:
        if (!input.includes("@")) {
          errors.push(validation.message);
        }
        break;
      case ValidationRule.Number:
        if (isNaN(Number(input))) {
          errors.push(validation.message);
        }
        break;
      case ValidationRule.Pattern:
        const regex = new RegExp(validation.value as string);
        if (!regex.test(input)) {
          errors.push(validation.message);
        }
        break;
    }
  }

  return errors;
}

let errors = validate("", [
  { rule: ValidationRule.Required, message: "Поле обязательно" },
  { rule: ValidationRule.MinLength, value: 3, message: "Минимум 3 символа" }
]);

console.log(errors);  // ["Поле обязательно", "Минимум 3 символа"]

let emailErrors = validate("invalid-email", [
  { rule: ValidationRule.Required, message: "Поле обязательно" },
  { rule: ValidationRule.Email, message: "Неверный формат email" }
]);

console.log(emailErrors);  // ["Неверный формат email"]

Пример 7: Система прав доступа

enum Permission {
  Read = 1,
  Write = 2,
  Delete = 4,
  Admin = 8
}

enum Role {
  Viewer = Permission.Read,
  Editor = Permission.Read | Permission.Write,
  Admin = Permission.Read | Permission.Write | Permission.Delete,
  SuperAdmin = Permission.Read | Permission.Write | Permission.Delete | Permission.Admin
}

function hasPermission(userRole: Role, permission: Permission): boolean {
  return (userRole & permission) === permission;
}

let currentUser: Role = Role.Editor;

console.log(hasPermission(currentUser, Permission.Read));    // true
console.log(hasPermission(currentUser, Permission.Write));   // true
console.log(hasPermission(currentUser, Permission.Delete));  // false
console.log(hasPermission(currentUser, Permission.Admin));   // false

let admin: Role = Role.Admin;
console.log(hasPermission(admin, Permission.Delete));  // true
console.log(hasPermission(admin, Permission.Admin));   // false

Пример 8: State Machine

enum OrderState {
  Created = "created",
  Paid = "paid",
  Shipped = "shipped",
  Delivered = "delivered",
  Cancelled = "cancelled"
}

enum OrderAction {
  Pay = "pay",
  Ship = "ship",
  Deliver = "deliver",
  Cancel = "cancel"
}

const validTransitions: Record<OrderState, OrderAction[]> = {
  [OrderState.Created]: [OrderAction.Pay, OrderAction.Cancel],
  [OrderState.Paid]: [OrderAction.Ship, OrderAction.Cancel],
  [OrderState.Shipped]: [OrderAction.Deliver],
  [OrderState.Delivered]: [],
  [OrderState.Cancelled]: []
};

function canPerformAction(
  currentState: OrderState,
  action: OrderAction
): boolean {
  return validTransitions[currentState].includes(action);
}

function performAction(
  state: OrderState,
  action: OrderAction
): OrderState {
  if (!canPerformAction(state, action)) {
    throw new Error(
      `Нельзя выполнить ${action} в состоянии ${state}`
    );
  }

  switch (action) {
    case OrderAction.Pay: return OrderState.Paid;
    case OrderAction.Ship: return OrderState.Shipped;
    case OrderAction.Deliver: return OrderState.Delivered;
    case OrderAction.Cancel: return OrderState.Cancelled;
  }
}

let state: OrderState = OrderState.Created;
console.log(state);  // "created"

state = performAction(state, OrderAction.Pay);
console.log(state);  // "paid"

state = performAction(state, OrderAction.Ship);
console.log(state);  // "shipped"

state = performAction(state, OrderAction.Deliver);
console.log(state);  // "delivered"

// Ошибка! Доставленный заказ нельзя отменить
// performAction(state, OrderAction.Cancel);  // Бросит ошибку

Итоги урока

Отлично! Мы подробно разобрали перечисления (enum) в TypeScript. Давайте подведём итоги.

Числовые enum:

• Автоматическая нумерация с нуля
• Можно задавать явные значения
• Поддерживают reverse mapping (число → имя)
• Создают JavaScript-объект в runtime

Строковые enum:

• Требуют явных значений для каждого элемента
• Не имеют reverse mapping
• Удобны для логирования и API
• Значения видны в консоли

Const enum:

• Полностью стирается при компиляции
• Экономит место в бандле
• Не создаёт JavaScript-объект
• Идеален для битовых флагов и констант

Ключевые моменты:

• Enum защищают от «магических чисел» и строк
• Автодополнение в IDE ускоряет разработку
• Exhaustiveness check с never гарантирует полную обработку
• Выбирайте enum vs union vs const объект в зависимости от задачи

👉 Следующий урок: Type aliases — псевдонимы типов

Научимся создавать собственные имена для типов, чтобы код был ещё чище и понятнее.

Проверяем понимание

8 вопросов

Enum для HTTP-статусов

Premium