TypeScript enum антипаттерн

Alexandr Vishniakov
2 min readMay 4, 2021

В книгах по TypeScript рецептам, говорится, что не стоит использовать enum, а лучше использовать объявление type.

Рассмотрим пример, правильного объявления перечисления:

const DIRECTIONS = {
UP: 'UP',
DOWN: 'DOWN'
} as const;
type DIRECTIONS = keyof typeof DIRECTIONS;
  • В первой строке, объявляем объект с readonly свойствами, что дает нам уверенность в том, что никто не сможет изменить значение DIRECTIONS.UP на какое либо другое значение и тоже самое про DIRECTIONS.DOWN , при работе с кодом на TypeScript.
  • Во второй строке, получаем обычное объявление type, но без дублирования кода. Мы могли бы написать:
type DIRECTIONS = 'UP' | 'DOWN'

но лучше, использовать возможности TypeScript, для соблюдения принципа DRY — Don't Repeat Yourself. И в этом нам помогает строка:

type DIRECTIONS = keyof typeof DIRECTIONS; 
// в итоге получаем type DIRECTIONS = 'UP' | 'DOWN'

Применение данного способа следующее:

const someDirection1: DIRECTIONS = "UP";const someDirection2: DIRECTIONS = "DOWN";const someDirection3: DIRECTIONS = DIRECTIONS.UP;

Плюс к возможности подсказки, как текстового значения, так и его валидации, мы получаем возможность использовать обращение к свойствам DIRECTIONS объекта, то есть DIRECTIONS.UP или DIRECTIONS.DOWN.

Без объявления константного объекта и при использовании только объявления type, через конкатенацию | значений, мы могли бы использовать только текстовое обозначение.

Так же этот вид объявления “перечисления” выдает чистый код, после транспиляции в JavaScript:

"use strict";
const DIRECTIONS = {
UP: 'UP',
DOWN: 'DOWN'
};
const someDirection1 = "UP";
const someDirection2 = "DOWN";
const someDirection3 = DIRECTIONS.UP;

Наличие const в коде на JS дает уверенность в том, что в JS не будет перезаписано значение объекта DIRECTIONS.

Чего не скажешь о транспиляции кода на TypeScript, с объявлением через enum:

enum DIRECTIONS {
UP,
DOWN,
};

в JavaScript:

"use strict";
var DIRECTIONS;
(function (DIRECTIONS) {
DIRECTIONS[DIRECTIONS["UP"] = 0] = "UP";
DIRECTIONS[DIRECTIONS["DOWN"] = 1] = "DOWN";
})(DIRECTIONS || (DIRECTIONS = {}));
;
  • В глаза бросается объявление глобальной, для модуля переменной DIRECTIONS, что тоже не очень хорошо, так как значение переменной, может быть без труда переписано/переопределено и никто не узнает об этом.
  • Значения такого перечисления содержат не строку, а число, что вносит неразбериху, так как после выполнения вышеописанного кода, получается следующие значения для DIRECTIONS:
{0: "UP", 1: "DOWN", UP: 0, DOWN: 1}

которое содержит как мэппинг из числа в строку, так и мэппинг из строки в число (которым и будет пользоваться разработчик).

  • Для получения текстового значения придется выполнить следующий кульбит в JS:
DIRECTIONS[DIRECTIONS.UP] // Вывод "UP"

В итоге с enum мы можем сделать так:

const someDirection: DIRECTIONS.UP = 123;console.log(someDirection); // 123;

а с использованием правильного объявления через const и type, мы получим ошибку

'DIRECTIONS' only refers to a type, but is being used as a namespace here.(2702)Exported variable 'someDirection' has or is using private name 'DIRECTIONS'.(4025)

для кода

const someDirection: DIRECTIONS.UP = 123;

Заключение

При использовании TypeScript, нужно так же думать о результатах транспиляции в JS, не станет ли это проблемой для мира JS, который в итоге и будет исполняться в браузере или при помощи node на сервере.

--

--

Alexandr Vishniakov

«Переписывание с нуля гарантирует лишь одно — ноль!» — Мартин Фаулер