← Курс/Enum и as const#160 из 257+25 XP

Enum в TypeScript

Числовые enum

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

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

const dir: Direction = Direction.Up
console.log(dir)             // 0
console.log(Direction[0])    // 'Up' — reverse mapping!

Можно задать начальное значение:

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

**Reverse mapping** — уникальная особенность числовых enum: по числу можно получить имя:

console.log(Direction[2])         // 'Left'
console.log(Direction['Left'])    // 2

Строковые enum

enum OrderStatus {
  Pending   = 'PENDING',
  Confirmed = 'CONFIRMED',
  Shipped   = 'SHIPPED',
  Delivered = 'DELIVERED',
  Cancelled = 'CANCELLED',
}

const status: OrderStatus = OrderStatus.Pending
console.log(status)  // 'PENDING'
// Нет reverse mapping у строковых enum!

Строковые enum предпочтительнее: значения читаемы в логах, нет неожиданного reverse mapping.

const enum — инлайнинг

const enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE',
}

// Использование:
const c = Color.Red  // Компилируется в: const c = 'RED'
// Объект Color НЕ создаётся в runtime!

**Важно**: const enum нельзя использовать через barrel re-exports и с isolatedModules (Vite, esbuild). В проектах на Vite используйте обычный enum или as const.

Проблемы enum

enum Fruit {
  Apple = 0,
  Banana = 1,
}

// Числовые enum принимают любое число — баг!
function eat(fruit: Fruit) { }
eat(999)  // TypeScript не ругается! Баг в дизайне.

// Строковые enum безопаснее:
enum FruitStr {
  Apple = 'APPLE',
}
eat2('APPLE')  // Ошибка TS — нужно FruitStr.Apple

Enum **увеличивает bundle** — компилируется в IIFE объект:

// Скомпилированный enum Direction:
var Direction;
(function (Direction) {
  Direction[Direction["Up"] = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));

Альтернатива: as const объект

Современный TypeScript рекомендует as const вместо enum:

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

// Тип значений:
type Direction = typeof Direction[keyof typeof Direction]
// 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'

function move(dir: Direction) { }
move(Direction.Up)  // OK
move('UP')          // OK — литерал тоже принимается
// move('diagonal') // Ошибка TS

// Union type — ещё проще:
type Status = 'pending' | 'success' | 'error'

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

| Сценарий | Рекомендация |

|---|---|

| Новый проект | as const + union type |

| Нужен reverse mapping | числовой enum |

| Компиляция без bundler | const enum |

| Легаси код / Angular | enum |

Примеры

State Machine для заказа с enum-like объектами (Object.freeze) и валидацией переходов

// В TypeScript: enum OrderStatus { Pending = 'PENDING', ... }
// В JS: Object.freeze имитирует const enum

const OrderStatus = Object.freeze({
  Pending:   'PENDING',
  Confirmed: 'CONFIRMED',
  Shipped:   'SHIPPED',
  Delivered: 'DELIVERED',
  Cancelled: 'CANCELLED',
})

// Допустимые переходы между статусами
const TRANSITIONS = Object.freeze({
  [OrderStatus.Pending]:   [OrderStatus.Confirmed, OrderStatus.Cancelled],
  [OrderStatus.Confirmed]: [OrderStatus.Shipped,   OrderStatus.Cancelled],
  [OrderStatus.Shipped]:   [OrderStatus.Delivered],
  [OrderStatus.Delivered]: [],  // финальное состояние
  [OrderStatus.Cancelled]: [],  // финальное состояние
})

const STATUS_LABELS = Object.freeze({
  [OrderStatus.Pending]:   'Ожидает подтверждения',
  [OrderStatus.Confirmed]: 'Подтверждён',
  [OrderStatus.Shipped]:   'Отправлен',
  [OrderStatus.Delivered]: 'Доставлен',
  [OrderStatus.Cancelled]: 'Отменён',
})

class Order {
  #status
  #history

  constructor(id) {
    this.id = id
    this.#status = OrderStatus.Pending
    this.#history = [{ status: this.#status, timestamp: new Date() }]
  }

  get status() { return this.#status }
  get label()  { return STATUS_LABELS[this.#status] }

  canTransitionTo(newStatus) {
    const allowed = TRANSITIONS[this.#status] ?? []
    return allowed.includes(newStatus)
  }

  transition(newStatus) {
    // Валидация: статус должен существовать
    if (!Object.values(OrderStatus).includes(newStatus)) {
      throw new Error(`Неизвестный статус: ${newStatus}`)
    }
    // Валидация: переход должен быть допустимым
    if (!this.canTransitionTo(newStatus)) {
      throw new Error(
        `Нельзя перейти из ${this.#status} в ${newStatus}`
      )
    }
    this.#status = newStatus
    this.#history.push({ status: newStatus, timestamp: new Date() })
    return this
  }

  confirm()  { return this.transition(OrderStatus.Confirmed) }
  ship()     { return this.transition(OrderStatus.Shipped) }
  deliver()  { return this.transition(OrderStatus.Delivered) }
  cancel()   { return this.transition(OrderStatus.Cancelled) }

  getHistory() {
    return this.#history.map(h => `${STATUS_LABELS[h.status]}`).join(' → ')
  }
}

// --- Демонстрация ---

console.log('=== Успешный заказ ===')
const order1 = new Order(1001)
console.log(`Статус: ${order1.label}`)

order1.confirm()
console.log(`Статус: ${order1.label}`)

order1.ship()
console.log(`Статус: ${order1.label}`)

order1.deliver()
console.log(`Статус: ${order1.label}`)
console.log(`История: ${order1.getHistory()}`)

console.log('\n=== Отменённый заказ ===')
const order2 = new Order(1002)
order2.confirm().cancel()
console.log(`История: ${order2.getHistory()}`)

console.log('\n=== Попытки недопустимых переходов ===')
const order3 = new Order(1003)
try {
  order3.ship()  // Нельзя: Pending → Shipped
} catch (e) {
  console.log(`Ошибка: ${e.message}`)
}

order3.confirm()
// Нельзя: Confirmed → Delivered (пропускаем Shipped)
try {
  order3.deliver()
} catch (e) {
  console.log(`Ошибка: ${e.message}`)
}

console.log('\n=== Проверка допустимых переходов ===')
const order4 = new Order(1004)
const allStatuses = Object.values(OrderStatus)
allStatuses.forEach(s => {
  const can = order4.canTransitionTo(s)
  if (can) console.log(`  Можно: ${OrderStatus.Pending}${s}`)
})