
Привет, меня зовут Дмитрий, и я руководитель фронтенд-разработки в компании Интелси. В данной статье я хочу подробно разобрать 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 с помощью 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 существует специальный тип, позволяющий создавать неизменяемые списки с заранее известными значениями. Такие значения гарантированно определены ещё на этапе компиляции. Этот тип можно использовать как:
свойство объекта или класса,
тип параметра функции,
тип возвращаемого значения.
Это делает его удобным инструментом для повышения надёжности и предсказуемости кода.
Комментарии (13)
Andr3y3
13.08.2025 08:35TypeScript вообще не нужен
Xiran
13.08.2025 08:35Для человека, с бекграундом строгого языка, который выражает семантику синтаксисом, где не объявленная переменная приводит к ошибке компиляции, C++, JS выглядит недоделанным языком, а TS его доделывает и делает удобнее C++. И со мной согласятся те, кто писал на C#, Java, Kotlin, ...
ermac
13.08.2025 08:35Про const enum не все так просто, он не сгенерирует типы и не будет никакого объекта, это compile time структура, есть отдельный раздел с нюансами в документации
Vitaly_js
13.08.2025 08:35Мне вот тоже более интересен практический момент. По опыту, использование enum всегда менее удобно. В основном из-за вот этой ситуации:
enum Actions { teacher = 'teach', doctor = 'treat' } interface MyInterface { currentAction: AnotherActions } interface MyAnotherInterface { currentAction: Actions } const actions = { teacher: 'teach', doctor: 'treat' } as const type Role = keyof typeof actions type AnotherActions = (typeof actions)[Role] const a = { currentAction: 'teach' } satisfies MyAnotherInterface // Type '"teach"' is not assignable to type 'Actions'.(2322) const b = { currentAction: 'teach' } satisfies MyInterface // Ok
Так как обычно мы работаем со строками, то использование литерала удобнее.
При этом проблемы с перебором одинаковые
for (let [key, value] of Object.entries(ProfessionAction)) { console.log(key, value) ProfessionAction[key] // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type }
muhachev
13.08.2025 08:35Автор дилетант. Не знает о чём писать. Изложил кучу несущественных и ненужных деталей, и ни слова о философии и относительной полезности енумов. Ну и вообще в ts/js это притянутая за уши бесполезная конструкция, вероятно замануха для сишников/плюсников. А то, что автор полный профан и шарлатан, очевидно с первых его строк, где он с умным, но невменяемым видом самозабвенно прикручивает дополнительную семантику значениям енумов.
fogonthestreet Автор
13.08.2025 08:35Будет здорово, если раскроете свою мысль.
А вообще, раз разработчики TypeScript добавили эту функцию, то наверное считали её важной. Иначе не стали бы добавлять enum в свой язык.
azizoid
и ни слова про object literal mapping?
enum очень уродливый сам по себе, плюс не спасает от ситуаций типа
ProfessionAction[5]
даже если там всего два значения
Надо писать
const ProfessionAction = {
doctor: "treat",
teacher: "teach"
} as const
type ProfessionActionProps = keyof typeof ProfessionAction
или наоборот если уж очень хочеться
fogonthestreet Автор
Всё же что-то упустил. Спасибо за дополнение.
Xiran
Сразу видно, человек не писал на C/C++/C#/Java.
Казалось бы, зачем это, если enum позволяет тупо все перечислить, и короче выглядит, и то, и се.
А за чисто JS-ный прикол спасибо.
Zukomux
Вот как раз на этих языках он реализован нормально, а в JS/TS это огрызок. Да ещё и неявное создание другого объекта под капотом. Значение enum будет являться строгим строковым литералом, а значит ни одно сравнение с переменным вида string работать не будут - потребуется явное приведение в месте использования.
Xiran
Я и говорю, что enum намного привычнее всяких объектов чисто по синтаксису, я не говорил про реализацию конкретно в жс/тс, C/C++/C#/чашке кофе, а тем более, я НЕ говорил про то, что реализация в тс якобы лучше чем бекенд языках
unratio
Вам стоит посмотреть, во что вырождается данная конструкция после компиляции tsc. As const подход дает меньше накладных расходов (т.к. после компиляции это будет просто объект) и также это путь к будущей node, с нативным исполнением ts (концепт стирания всех ts аннотаций)