javascript

Для чего нужны enum в TypeScript. Подробно и простым языком

  • четверг, 14 августа 2025 г. в 00:00:04
https://habr.com/ru/articles/936650/

Привет, меня зовут Дмитрий, и я руководитель фронтенд-разработки в компании Интелси. В данной статье я хочу подробно разобрать enum в TypeScript, чтобы было понятно, что это такое, для чего нужно и почему это работает именно так.

Давайте создадим простой enum, который в качестве ключа будет содержать название профессии, а в качестве значения то, чем обычно занимается представитель данной профессии (врач лечит, учитель обучает):

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

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

Значения enum нельзя переопределить или удалить:

ProfessionAction.doctor = 'teach';
// Ошибка:
// Cannot assign to 'doctor' because it is a read-only property.ts(2540)

delete ProfessionAction.doctor;
// Ошибка:
// The operand of a 'delete' operator cannot be a read-only property.ts(2704)

Как мы знаем, TypeScript не выполняется сам по себе, а компилируется в JavaScript. Но в JavaScript нет такой сущности. Во что же он превратится? Ответ - в объект. Вот результат компиляции нашего enum в JavaScript с помощью https://www.typescriptlang.org/play:

"use strict";

var ProfessionAction;

(function (ProfessionAction) {
  ProfessionAction["doctor"] = "treat";
  ProfessionAction["teacher"] = "teach";
})(ProfessionAction || (ProfessionAction = {}));

Тут мы видим анонимную самовызывающуюся функцию, которая принимает в качестве аргумента вот такое условие:

ProfessionAction || (ProfessionAction = {})

Получается, что если в переменной ProfessionAction, которая задаётся выше, не содержит значение, то она присваивает ей пустой объект. А в теле функции происходит наполнение этого объекта свойствами, которые мы задавали в enum, Благодаря тому, что значение, которое мы передаём в качестве аргумента, выглядит именно так, то можно дополнять enum после создания:

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

enum ProfessionAction {
  tailor = 'sew',
}

И посмотрим результат компиляции данного кода в JavaScript:

"use strict";

var ProfessionAction;

(function (ProfessionAction) {
  ProfessionAction["doctor"] = "treat";
  ProfessionAction["teacher"] = "teach";
})(ProfessionAction || (ProfessionAction = {}));

(function (ProfessionAction) {
  ProfessionAction["tailor"] = "sew";
})(ProfessionAction || (ProfessionAction = {}));

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

Но если мы не хотим, чтобы такое дополнение было возможно, то нужно использовать ключевое слово “const”:

const enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}
// Ошибка:
// Enum declarations can only merge with namespace or other enum declarations.ts(2567)

enum ProfessionAction {
  tailor = 'sew',
}
// Ошибка:
// Enum declarations can only merge with namespace or other enum declarations.ts(2567)

Давайте попробуем перебрать элементы enum в цикле. Поскольку он приводится к объекту, то в голову приходит вариант использования того же подхода, что и с объектом. Самый популярный - цикл for in:

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

for (let key in ProfessionAction) {
  console.log(key)
  // Работает, выводит ключи: "doctor", "teacher"
}

for (let key in ProfessionAction) {
  console.log(ProfessionAction[key])
  // Ошибка:
  // Element implicitly has an 'any' type because expression of type
  // 'string' can't be used to index type 'typeof ProfessionAction'.
  // No index signature with a parameter of type 'string' was found
  // on type 'typeof ProfessionAction'.(7053)
}

Но тут есть проблема. Цикл for..in не может безопасно гарантировать, что ключи являются только известными свойствами, потому что объекты могут иметь дополнительные свойства во время выполнения. Поэтому используется string.

По этой причине тип ключа в ProfessionAction и тип значения переменной key не совпадают. Лучше не использовать данный подход. Хотя enum и превращается в объект, но с точки зрения TypeScript это другая сущность. Лучше использовать методы объектов Object.entries, Object.keys или Object.values, а затем работать с получившейся сущностью как с массивом. Например:

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

for (let [key, value] of Object.entries(ProfessionAction)) {
  console.log(key, value)
}

Также в enum есть возможность не указывать значения, например:

enum ProfessionAction {
  doctor,
  teacher,
}

В этом случае в качестве значений будут задаваться индексы вместо значений. Для первого элемента 0, для второго 1 и так далее. По сути, такая запись эквивалентна такой:

enum ProfessionAction {
  doctor = 0,
  teacher = 1,
}

Кстати, в случае числовых значений value в enum, при его компиляции в JavaScript создастся объект с двусторонним маппингом. Результат может быть весьма неожиданным:

enum ProfessionAction {
  doctor = 0,
  teacher = 1,
}

for (let [key, value] of Object.entries(ProfessionAction)) {
  console.log(key, value)
}

// Результат в терминале:
// "0",  "doctor"
// "1",  "teacher"
// "doctor",  0
// "teacher",  1

Так происходит потому, что числовые enum автоматически генерируют обратные маппинги для доступа к именам по значениям. А строковые enum не создают обратных маппингов, так как строковые значения не требуют обратного преобразования (они уже семантически значимы).

Обратите внимание, что если мы не указали какое-то значение value, которое следует за другим числовым значением, это пропущенное значение станет числовым, которое станет на единицу больше, чем предыдущее:

enum ProfessionAction {
  doctor = 2,
  teacher,
}

for (let [key, value] of Object.entries(ProfessionAction)) {
  console.log(key, value)
}

// Результат в терминале:
// "2",  "doctor"
// "3",  "teacher"

Что же мы можем делать с enum? Каково его применение?

Можно использовать значения enum в качестве ключей объектов:

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

const professionActions = {
  [ProfessionAction.doctor]: "Лечит пациентов",
  [ProfessionAction.teacher]: "Учит студентов",
}

И в качестве ключей классов:

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

class ProfessionActions {
  // Используем значения enum как ключи
  [ProfessionAction.doctor]: string;
  [ProfessionAction.teacher]: string;

  constructor() {
    this[ProfessionAction.doctor] = "Лечит пациентов";
    this[ProfessionAction.teacher] = "Учит студентов";
  }
}

Можно использовать в качестве типов параметров и возвращаемого значений функции:

enum ProfessionAction {
  doctor = 'treat',
  teacher = 'teach'
}

function getProfessionActionArray (prop: ProfessionAction): Array<ProfessionAction> {
  return [prop, prop]
}

console.log(getProfessionActionArray(ProfessionAction.doctor))

Таким образом, в TypeScript существует специальный тип, позволяющий создавать неизменяемые списки с заранее известными значениями. Такие значения гарантированно определены ещё на этапе компиляции. Этот тип можно использовать как:

свойство объекта или класса,

тип параметра функции,

тип возвращаемого значения.

Это делает его удобным инструментом для повышения надёжности и предсказуемости кода.