Примитивный Тип Enum

При создании приложений тяжело обойтись без большого количества специальных конфигурационных значений. Подобные значения разработчики выносят в отдельные классы со статическими свойствами или модули с константами, избавляя таким образом свой код от магических значений.

TypeScript привносит новую синтаксическую конструкцию называемую Enum (перечисление). enum представляет собой набор логически связанных констант, в качестве значений которых могут выступать как числа, так и строки.

Enum (enum) примитивный перечисляемый тип

Enum — это конструкция, состоящая из набора именованных констант, именуемая списком перечисления и определяемая такими примитивными типами, как number и string. Enum объявляется с помощью ключевого слова enum.

Перечисления с числовым значением

Идентификаторы-имена для перечислений enum принято задавать во множественном числе. В случае, когда идентификаторам констант значение не устанавливается явно, они ассоциируются с числовым значениями, в порядке возрастания, начиная с нуля.

ts
enum Fruits {
    Apple, // 0
    Pear, // 1
    Banana // 2
}

Также можно установить любое значение вручную.

ts
enum Citrus {
    Lemon = 2, // 2
    Orange = 4, // 4
    Lime = 6 // 6
}

Если указать значение частично, то компилятор будет стараться соблюдать последовательность.

ts
enum Berries {
    Strawberry = 1,
    Raspberry, // 2
    
    Blueberry = 4,
    Cowberry // 5
}

Компилятор рассчитывает значение автоматически только на основе значения предыдущего члена перечисления. То есть, если первой и третей константе было установленно значение 10 и 20.

ts
enum Keys {
  A = 10,
  B, // 11
  C = 20,
  D // 21
}

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

ts
enum Keys {
  A = 10,
  B, // 11
  C = 10,
  D // 11
}

Вдобавок ко всему enum позволяет задавать псевдонимы (alias). Псевдонимам устанавливается значение константы, на которую они ссылаются.

ts
enum Languages {
    Apple, // en, value = 0
    Apfel = Apple, // de, value = 0
    LaPomme = Apple // fr, value = 0
}

При обращении к константе перечисления через точечную нотацию, будет возвращено значение. А при обращении к перечислению с помощью скобочной нотации и указания значения в качестве ключа, будет возвращено строковое представление идентификатора константы.

ts
let value: number = Fruits.Apple; // 0
let identificator: string = Fruits[value]; // “Apple”

Поскольку enum представляет реальные значения без которых программа будет неработоспособна, он обязан оставаться в коде после компиляции. Поэтому, что бы быстрее понять enum, нужно посмотреть на него в скомпилированном конечном виде. Но прежде создадим его самостоятельно.

1 шаг. Тем, кто ранее работал с enum уже известно, что он позволяет получать строковое представление константы, а также значение ассоциированное с ней. Поэтому для его создания требуется ассоциативный массив, коими в JavaScript являются объекты. Назовем объект Fruits и передадим его в качестве аргумента в функцию initialization, которая будет содержать код его инициализации.

ts
let Fruits = {};

function initialization(Fruits){

}

2 шаг. Создадим поле с именем Apple и присвоим ему в качестве значения число 0.

ts
let Fruits = {};

function initialization(Fruits) {
    Fruits["Apple"] = 0;
}

3 шаг. Ассоциация константа-значение произведена, осталось создать зеркальную ассоциацию значение-константа. Для этого создадим ещё одно поле у которого в качестве ключа будет выступать значение 0, а в качестве значения — строковое представление константы, то есть имя.

ts
let Fruits = {};

function initialization(Fruits) {
    Fruits[ "Apple" ] = 0;
    Fruits[ 0 ] = "Apple";
}

4 шаг. Теперь сократим код, но начала вспомним, что результатом операции присваивания является значение правого операнда. Поэтому сохраним результат первого выражения в переменную value, а затем используем её в качестве ключа во втором выражении.

ts
let Fruits = {};

function initialization(Fruits) {
    let value = Fruits["Apple"] = 0; //, то же самое, что value = 0
    Fruits[value] = "Apple"; //, то же самое, что Fruits[0] = "Apple";
}

5 шаг. Продолжим сокращать и в первом выражении откажемся от переменной value, а во втором выражении на её место поместим первое выражение.

ts
let Fruits = {};

function initialization( Fruits ){
    Fruits[Fruits["Apple"] = 0] = "Apple";
}

6 шаг. Теперь проделаем, то же самое для двух других констант.

ts
let Fruits = {};

function initialization(Fruits) {
    Fruits[Fruits["Apple"] = 0] = "Apple";
    Fruits[Fruits["Lemon"] = 1] = "Lemon";
    Fruits[Fruits["Orange"] = 2] = "Orange";
}

7 шаг. Теперь превратим функции initialization в самовызывающееся функциональное выражение и лучше анонимное.

ts
let Fruits = {};

(function(Fruits) {
    Fruits[Fruits["Apple"] = 0] = "Apple";
    Fruits[Fruits["Pear"] = 1] = "Pear";
    Fruits[Fruits["Banana"] = 2] = "Banana";
})(Fruits);

8 шаг. И перенесем инициализацию объекта прямо на место вызова.

ts
let Fruits;
(function(Fruits) {
    Fruits[Fruits["Apple"] = 0] = "Apple";
    Fruits[Fruits["Pear"] = 1] = "Pear";
    Fruits[Fruits["Banana"] = 2] = "Banana";
})(Fruits || (Fruits = {}));

Перечисление готово. Осталось сравнить созданное перечисление с кодом полученным в результате компиляции.

ts
// enum сгенерированный typescript compiler
let Fruits;
(function (Fruits) {
  Fruits[Fruits["Apple"] = 0] = "Apple";
  Fruits[Fruits["Pear"] = 1] = "Pear";
  Fruits[Fruits["Banana"] = 2] = "Banana";
})(Fruits || (Fruits = {}));

Теперь добавим в рассматриваемое перечисление псевдоним LaPomme (яблоко на французском языке) для константы Apple.

ts
enum Fruits {
    Apple, // 0
    Pear, // 1
    Banana, // 2
    
    LaPomme = Apple // 0
}

И снова взглянем на получившийся в результате компиляции код. Можно увидеть, что псевдоним создается так же, как обычная константа, но в качестве значения ему присваивается значение идентичное константе на которую он ссылается.

ts
(function (Fruits) {
  Fruits[Fruits["Apple"] = 0] = "Apple";
  Fruits[Fruits["Lemon"] = 1] = "Lemon";
  Fruits[Fruits["Orange"] = 2] = "Orange";
  Fruits[Fruits["LaPomme"] = 0] = "LaPomme"; // псевдоним
})(Fruits || (Fruits = {}));

Перечисления со строковым значением

Помимо значения принадлежащего к типу number, TypeScript позволяет указывать значения с типом string.

ts
enum FruitColors {
    Red = "#ff0000",
    Green = "#00ff00",
    Blue = "#0000ff"
}

Но в случае, когда константам присваиваются строки, ассоциируется только ключ со значением. Обратная ассоциация (значение-ключ) — отсутствует. Простыми словами, по идентификатору (имени константы) можно получить строковое значение, но по строковому значению получить идентификатор (имя константы) невозможно.

ts
var FruitColors;
(function (FruitColors) {
    FruitColors["Red"] = "#ff0000";
    FruitColors["Green"] = "#00ff00";
    FruitColors["Blue"] = "#0000ff";
})(FruitColors || (FruitColors = {}));

Тем не менее остается возможность создавать псевдонимы (alias).

ts
enum FruitColors {
    Red = "#ff0000",
    Green = "#00ff00",
    Blue = "#0000ff",
    
    Rouge = Red, // fr "#ff0000"
    Vert = Green, // fr "#00ff00"
    Bleu = Blue // fr "#0000ff"
}

И снова изучим скомпилированный код. Можно убедится, что псевдонимы создаются так же, как и константы. А значение присваиваемое псевдонимам идентично значению констант на которые они ссылаются.

ts
var FruitColors;
(function (FruitColors) {
    FruitColors["Red"] = "#ff0000";
    FruitColors["Green"] = "#00ff00";
    FruitColors["Blue"] = "#0000ff";
    FruitColors["Rouge"] = "#ff0000";
    FruitColors["Vert"] = "#00ff00";
    FruitColors["Bleu"] = "#0000ff";
})(FruitColors || (FruitColors = {}));

Смешанное перечисление (mixed enum)

Если в одном перечислении объявлены числовые и строковые константы, то такое перечисление называется смешанным (mixed enum).

Со смешанным перечислением связаны две неочевидные особенности.

Первая из них заключается в том, что константам, которым значение не задано явно, присваивается числовое значение по правилам перечисления с числовыми константами.

ts
enum Stones {
    Peach, // 0
    Apricot = "apricot"
}

Вторая особенность заключается в том, что если константа, которой значение не было присвоено явно, следует после константы со строковым значением, то такой код не скомпилируется. Причина заключается в том, что как было рассказано в главе “Перечисления с числовым значением”, если константе значение не было установлено явно, то её значение будет рассчитано, как значение предшествующей ей константе +1, либо 0, в случае её отсутствия. А так как у предшествующей константы значение принадлежит к строковому типу, то рассчитать число на его основе не представляется возможным.

ts
enum Stones {
    Peach, // 0
    Apricot = "apricot",
    Cherry, // Error
    Plum // Error
}

Для разрешения этой проблемы в смешанном перечислении, константе, которая была объявлена после константы со строковым значением, необходимо задавать значение явно.

ts
enum Stones {
    Peach, // 0
    Apricot = "apricot",
    Cherry = 1, // 1
    Plum // 2
}

Перечисление в качестве типа данных

Может возникнуть мысль использовать перечисление в качестве типа данных переменной или параметра. Это вполне нормальное желание, но нужно быть очень осторожным: в TypeScript с перечислением связан один достаточно неприятный нюанс. Дело в том, что пока в перечислении есть хотя бы одна константа с числовым значением, он будет совместим с типом number. Простыми словами, любое число проходит проверку совместимости типов с любым перечислением.

Функцию, тип параметра которой является смешанным перечислением, благополучно получится вызвать как с константой перечисления в качестве аргумента, так и с любым числом. Вызвать эту же функцию с идентичной константе перечисления строкой уже не получится.

ts
enum Fruits {
    Apple,
    Pear,
    Banana = "banana"
}

function isFruitInStore(fruit: Fruits): boolean {
    return true;
}

isFruitInStore(Fruits.Banana); // ок
isFruitInStore(123456); // ок
isFruitInStore("banana"); // Error

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

ts
enum Berries {
    Strawberry = "strawberry",
    Raspberry = "raspberry",
    Blueberry = "blueberry"
}

function isBerryInStory(berry: Berries): boolean {
    return true;
}

isBerryInStory(Berries.Strawberry); // ок
isBerryInStory(123456); // Error
isBerryInStory("strawberry"); // Error

Поведение не совсем очевидное, поэтому не стоит забывать об этом при использовании перечислений в которых присутствуют константы с числовым значением в качестве типа.

Перечисление const с числовым и строковым значением

Перечисление enum объявленное с помощью ключевого слова const после компиляции не оставляет в коде привычных конструкций. Вместо этого компилятор встраивает литералы значений в места, в которых происходит обращение к значениям перечисления. Значения констант перечисления могут быть как числовыми, так и строковыми типами данных. Так же, как и в обычных перечислениях, в перечислениях объявленных с помощью ключевого слова const, есть возможность создавать псевдонимы (alias) для уже объявленных констант.

Если создать два перечисления Apple и Pear, у каждого из которых будет объявлена константа Sugar с числовым значением, то на основе этих констант можно рассчитать количество сахара в яблочно-грушевом соке. Присвоив результат операции сложения количества сахара в промежуточную переменную, мы получим хорошо читаемое, задекларированное выражение.

ts
const enum Apple {
    Sugar = 10
}

const enum Pear {
    Sugar = 10
}

let calciumInApplePearJuice: number = Apple.Sugar + Pear.Sugar;

После компиляции от перечисления не остается и следа, так как константы будут заменены числовыми литералами. Такое поведение называется inline встраивание.

ts
let calciumInApplePearJuice = 10 + 10;

Обращение к значению через точечную нотацию требует большего времени, чем обращение к литеральному значению напрямую. Поэтому код с inline конструкциями выполняется быстрее по сравнению с кодом, в котором происходит обращение к членам объекта. Прибегать к подобному подходу рекомендуется только в тех частях кода, которые подвержены высоким нагрузкам. За счет перечисления, объявленного с ключевым словом const, исходный код будет легко читаемым, а конечный код — более производительным.

Тип enum является уникальным для TypeScript, в JavaScript подобного типа не существует.

Когда стоит применять enum?

Может возникнуть вопрос - "Когда использовать enum и стоит ли это делать с учетом закрепившейся привычки работы со статическими классами и константами?".

Ответ очевиден — безусловно стоит применять тогда, когда нужна двухсторонняя ассоциация строкового ключа с его числовым или строковым значением (проще говоря, карта строковый ключ — числовое значение_числовой ключ — строковое значение_).

Кроме того, enum лучше всего подходит для определения дискриминантных полей речь о которых пойдет позже.

Ну а тем, кто считает, что скомпилированная конструкция enum отягощает их код и при этом они пользовались ранее транскомпилятором Babel, то ответьте себе на вопрос: "Почему вы это делали, если он добавляет в сотню раз больше лишнего кода?". Рассуждение о том, что несколько лишних строк кода испортит или опорочит программу, является пустой тратой драгоценного времени.

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