Skip to main content

Чому enum часто вважають антипатерном у TS?

Коротко: чому enum часто називають антипатерном

Тому що enum - це "магія часу виконання", яка псує типобезпеку, обтяжує код і гірше інтегрується з екосистемою JS, ніж альтернативи на кшталт const enum чи as const-об'єктів.


1. enum існує і в runtime, і в compile-time

Головна особливість (і проблема) enum - він генерує JavaScript-код під час компіляції.

Приклад:

javascript
enum Direction { Up, Down, Left, Right, }

Компілюється в JS як:

javascript
"use strict"; 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 - не суто типова конструкція, а генерує реальний об'єкт, що:

  • додає зайвий код у бандл,
  • може поводитися не так, як очікуєш.

2. Двостороннє відображення (reverse mapping) = потенційний баг

TypeScript створює двосторонню таблицю:

javascript
Direction.Up === 0 Direction[0] === "Up"

Це зручно, але небезпечно:

javascript
enum Role { Admin, User } const role = Role[0]; // "Admin"

Значення можуть бути звернені навпаки, що створює приховані вразливості й плутанину.


3. Значення enum не типобезпечні

Enum поводиться майже як об'єкт зі значеннями number | string, і TypeScript часто не захищає від помилок присвоєння.

Приклад:

javascript
enum Role { Admin, User } const role: Role = 42; // допустимо (!)

Це неочевидно і ламає сенс enum, тому що 42 - неіснуюче значення, але TS його не відфільтрує.


4. Змішування типів (numeric + string enums)

Можна писати ось так:

javascript
enum Mixed { Yes = "YES", No = 0, }

Таке поєднання чисел і рядків призводить до непередбачуваної поведінки і ускладнює перевірку типів.


5. enum погано працює з tree-shaking

Оскільки enum компілюється в реальний JS-об'єкт, він не може бути видалений оптимізатором під час збірки (наприклад, Terser чи esbuild).

Приклад:

javascript
enum Colors { Red, Blue, Green }

Навіть якщо Colors не використовується, він все одно потрапить у бандл.

А от об'єкт з as const - видаляється, якщо не використовується:

javascript
const Colors = { Red: 0, Blue: 1, Green: 2 } as const;

6. Погана сумісність з plain JS / JSON / API

Enum - це об'єкт часу виконання, і його значення не збігаються з простими рядками.

Приклад:

javascript
enum Status { Active = "active", Inactive = "inactive" } function setStatus(status: Status) { ... } setStatus("active"); // Помилка - TS чекає Status.Active

Хоча рядок "active" за змістом збігається, TypeScript вимагає саме Status.Active. Це заважає використовувати enum з даними з API.

Краще використовувати рядковий літерал:

javascript
type Status = "active" | "inactive";

7. Труднощі при серіалізації та дебазі

javascript
enum Status { Active, Inactive } JSON.stringify(Status); // {"0":"Active","1":"Inactive","Active":0,"Inactive":1}

Замість простого переліку - марна мішанина, з якої важко дістати потрібні значення.


8. Поведінка enum ламає концепцію "value-level vs type-level"

TypeScript зазвичай розділяє:

  • type level (лише під час компіляції)
  • value level (у runtime)

enum - змішує ці два рівні.

Приклад:

javascript
enum Fruit { Apple, Orange } function eat(fruit: Fruit) { ... } const a = Fruit.Apple; // змінна і тип в одному

Це "магія", яка ламає принципи передбачуваності TS.


9. Альтернатива: as const + typeof

Сучасний і безпечніший спосіб - літеральні об'єкти з as const.

Приклад:

javascript
const Direction = { Up: "up", Down: "down", Left: "left", Right: "right", } as const; type Direction = typeof Direction[keyof typeof Direction];

Переваги:

  • Не створює JS-коду (лише типи);
  • Працює з API та JSON;
  • Дружній до tree-shaking;
  • Не ламає типізацію.

10. const enum - компроміс (але теж з нюансами)

Якщо все ж потрібен enum, краще використовувати const enum:

javascript
const enum Direction { Up, Down, Left, Right, }

TypeScript вбудовує значення напряму:

javascript
const move = Direction.Up; // компілюється в: const move = 0;

Але:

  • не працює з isolatedModules: true (наприклад, з Babel);
  • ламає дебаг (значення перетворюються на числа);
  • може бути несумісний з іншими інструментами (наприклад, SWC, ts-node).

11. enum ламає ідею "тип як документ контракту"

Наприклад, коли ти використовуєш enum у публічному API, споживачі бачать не літеральні значення, а числові коди. Це робить тип менш самодокументованим.

Приклад:

javascript
enum Status { Success, Fail } function getStatus(): Status { ... }

Повертає 0 | 1, але IDE не підкаже "Success" | "Fail" - розуміння коду ускладнюється.


12. Підсумок: коли enum дійсно потрібен

enum виправданий лише в рідкісних випадках:

  • при сумісності з наявним C#/Java кодом;
  • при використанні із зовнішніми бібліотеками, де enum - частина API;
  • у специфічних compile-time сценаріях (const enum).

Для всього іншого: використовуй string literal union або as const об'єкт.


Фінальна порівняльна таблиця

ПідхідГенерує код?Безпечний?Зручний з API?Tree-shakingРекомендується
enumТакЧастковоНіНіНі
const enumНі (inline)ТакЧастковоТакОбережно
as const об'єктНіТакТакТакРекомендується
Union типів (`'a''b'`)НіТакТакТак

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.