Перейти к содержанию

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

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

TypeScript предлагает решение в виде перечисления Enum. Идеологически, enum представляет из себя набор логически связанных констант. Значениями констант могут быть как числа, так и строки.

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

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

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

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

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

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

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

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

enum Berrys {
  Strawberry = 1,
  Raspberry, // 2

  Blueberry = 4,
  Cowberry, // 5
}

Компилятор рассчитывает значение автоматически только на основе значения предыдущего члена перечисления. То есть, в перечислении с четырьмя константами, одной из которых значение установлено явно, предположим, третья по счету константа со значением 0, значения для остальных констант будут устанавливаться следующим образом: первой константе будет задано значение 0; второй, так как у первой значение 0, будет установлено значение 1; третьей константе мы явно установим значение 0, поэтому компилятор пропускает её и идет дальше; четвертой, последней константе он устанавливает значение 1, так как у предыдущего члена перечисления установлено значение 0. Таким образом компилятор неявно создал повторяющиеся значения 1. При попытке получить строковое представление константы перечисления по числовому повторяющемуся числовому значению, в качестве результата будет возвращена та константа, которая была определена последней.

enum Nuts {
  Peanuts, // 0
  Walnut, // 1
  Hazelnut = 0, // 0
  Cedar, // 1
}
console.log(Nuts[Nuts.Peanuts]) // Hazelnut
console.log(Nuts[Nuts.Walnut]) // Cedar

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

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

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

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

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

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

var Fruits = {}

function initialization(Fruits) {}

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

var Fruits = {}

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

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

var Fruits = {}

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

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

var Fruits = {}

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

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

var Fruits = {}

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

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

var Fruits = {}

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

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

var Fruits = {}

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

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

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

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

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

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

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

  LaPomme = Apple, // 0
}

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

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

По причине того, что конструкции Enum не существует в JavaScript, хочется сказать лишь одно — данный тип перечисления безусловно стоит применять тогда, когда нужна двухсторонняя ассоциация ключа с его числовым значением или, проще говоря, карта “строковый ключ - числовое значение \ числовой ключ - строковое значение”.

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

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

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

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

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

Точно также, как Enum с числовыми значениями, Enum со строковыми значениями позволяет создавать псевдонимы (alias).

Добавим в перечисление FruitColors псевдонимы.

enum FruitColors {
  Red = '#ff0000',
  Green = '#00ff00',
  Blue = '#0000ff',

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

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

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).

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

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

enum Stones {
  Peach, // 0
  Apricot = 'apricot',
}

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

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

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

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

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

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

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

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

enum Fruits {
  Apple,
  Pear,
  Banana = 'banana',
}

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

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

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

enum Berrys {
  Strawberry = 'strawberry',
  Raspberry = 'raspberry',
  Blueberry = 'blueberry',
}

function isBerryInStory(berry: Berrys): boolean {
  return true
}

isBerryInStory(Berrys.Strawberry) // ок
isBerryInStory(123456) // Error
isBerryInStory('strawberry') // Error

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

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

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

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

const enum Apple {
  Sugar = 10,
}

const enum Pear {
  Sugar = 10,
}

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

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

let calciumInApplePearJuice = 10 + 10

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

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

Итог

  • Примитивный тип данных перечисление (Enum) — это конструкция, представляющая собой список именованных констант.
  • Объявление перечисления происходит с помощью ключевого слова enum.
  • Константы перечисления могут быть ассоциированы с такими типами как Number и String.
  • В случае, когда в перечислении отсутствует явное указание значений, они ассоциируются с числовыми значениям в возрастающем порядке начиная с нуля.
  • В случае, когда значения в перечислении заданы не всем константам, то те константы, которым не было задано значение, будут ассоциированы с числовыми значениями основываясь на результате значения предыдущей константы или точки отсчета — нуля.
  • Константе нужно присваивать значение явно, если впереди стоящей объявленной было присвоено строковое значение.
  • Нельзя забывать, что с перечислениями, в которых присутствуют константы со значением типа Number, совместимость проходит любое число.
  • Перечисления, объявленные с добавлением ключевого слова const, встраивают значения констант inline
  • Использовать inline встраивание рекомендуется в коде, который подвержен высокой нагрузке.