Примитивный тип Enum¶
При создании приложений тяжело обойтись без большого количества специальных конфигурационных значений. Подобные значения разработчики выносят в отдельные классы со статическими свойствами или модули с константами, избавляя таким образом свой код от магических значений.
TypeScript привносит новую синтаксическую конструкцию, называемую 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
}
Компилятор рассчитывает значение автоматически только на основе значения предыдущего члена перечисления. То есть, если первой и третьей константе было установленно значение 10
и 20
.
enum Keys {
A = 10,
B, // 11
C = 20,
D, // 21
}
Поскольку enum
позволяет разработчику задавать одинаковые значения своим константам, при частично устанавливаемых значениях нужно быть предельно внимательным чтобы не допустить ещё и повторений со стороны самого enum
.
enum Keys {
A = 10,
B, // 11
C = 10,
D, // 11
}
Вдобавок ко всему 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
, нужно посмотреть на него в скомпилированном конечном виде. Но прежде создадим его самостоятельно.
1 шаг. Тем, кто ранее работал с enum
, уже известно, что он позволяет получать строковое представление константы, а также значение, ассоциированное с ней. Поэтому для его создания требуется ассоциативный массив, коими в JavaScript являются объекты. Назовем объект Fruits
и передадим его в качестве аргумента в функцию initialization
, которая будет содержать код его инициализации.
let Fruits = {};
function initialization(Fruits) {}
2 шаг. Создадим поле с именем Apple
и присвоим ему в качестве значения число 0
.
let Fruits = {};
function initialization(Fruits) {
Fruits['Apple'] = 0;
}
3 шаг. Ассоциация константа-значение произведена, осталось создать зеркальную ассоциацию значение-константа. Для этого создадим ещё одно поле, у которого в качестве ключа будет выступать значение 0
, а в качестве значения — строковое представление константы, то есть имя.
let Fruits = {};
function initialization(Fruits) {
Fruits['Apple'] = 0;
Fruits[0] = 'Apple';
}
4 шаг. Теперь сократим код, но сначала вспомним, что результатом операции присваивания является значение правого операнда. Поэтому сохраним результат первого выражения в переменную value
, а затем используем её в качестве ключа во втором выражении.
let Fruits = {};
function initialization(Fruits) {
let value = (Fruits['Apple'] = 0); // то же самое что value = 0
Fruits[value] = 'Apple'; // то же самое что Fruits[0] = "Apple";
}
5 шаг. Продолжим сокращать и в первом выражении откажемся от переменной value
, а во втором выражении на её место поместим первое выражение.
let Fruits = {};
function initialization(Fruits) {
Fruits[(Fruits['Apple'] = 0)] = 'Apple';
}
6 шаг. Теперь проделаем то же самое для двух других констант.
let Fruits = {};
function initialization(Fruits) {
Fruits[(Fruits['Apple'] = 0)] = 'Apple';
Fruits[(Fruits['Lemon'] = 1)] = 'Lemon';
Fruits[(Fruits['Orange'] = 2)] = 'Orange';
}
7 шаг. Теперь превратим функцию intialization
в самовызывающееся функциональное выражение и лучше анонимное.
let Fruits = {};
(function (Fruits) {
Fruits[(Fruits['Apple'] = 0)] = 'Apple';
Fruits[(Fruits['Pear'] = 1)] = 'Pear';
Fruits[(Fruits['Banana'] = 2)] = 'Banana';
})(Fruits);
8 шаг. И перенесем инициализацию объекта прямо на место вызова.
let Fruits;
(function (Fruits) {
Fruits[(Fruits['Apple'] = 0)] = 'Apple';
Fruits[(Fruits['Pear'] = 1)] = 'Pear';
Fruits[(Fruits['Banana'] = 2)] = 'Banana';
})(Fruits || (Fruits = {}));
Перечисление готово. Осталось сравнить созданное перечисление с кодом, полученным в результате компиляции.
// 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
.
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 = {}));
Перечисления со строковым значением¶
Помимо значения, принадлежащего к типу 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 = {}));
тем не менее остается возможность создавать псевдонимы (alias).
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).
Со смешанным перечислением связаны две неочевидные особенности.
Первая из них заключается в том, что константам, которым значение не задано явно, присваивается числовое значение по правилам перечисления с числовыми константами.
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 и стоит ли это делать с учетом закрепившейся привычки работы со статическими классами и константами?".
Ответ очевиден - безусловно стоит применять тогда, когда нужна двухсторонняя ассоциация строкового ключа с его числовым или строковым значением (проще говоря, карта строковый ключ - числовое значение / числовой ключ - строковое значение).
Кроме того, enum
лучше всего подходит для определения дискриминантных полей, речь о которых пойдет позже.
Ну а тем, кто считает, что скомпилированная конструкция enum
отягощает их код и при этом они пользовались ранее транскомпилятором Babel
, то ответьте себе на вопрос - Почему вы это делали, если он добавляет в сотню раз больше лишнего кода?. Рассуждение о том, что несколько лишних строк кода испортит или опорочит программу, является пустой тратой драгоценного времени.
Поэтому если есть желание использовать enum
, то делайте это. Мне не доводилось встречать приложения, в которых не было бы enum
, константных классов и просто модулей с константами одновременно. И это более чем нормально.