Дмитрий Нечаев
Enums в TypeScript
Введение
Перечисления (Enums) являются одной из немногих функций TypeScript, которые не являются расширением типовых функций JavaScript.
Перечисления позволяют разработчику определить набор именованных констант. Использование перечислений может упростить документирование намерений или создание набора различных вариантов. TypeScript предоставляет как числовые, так и строковые перечисления.
Числовые перечисления
Сначала мы начнем с числовых перечислений, которые, вероятно, более знакомы вам, если вы изучаете другие языки. Перечисление можно определить с помощью ключевого слова enum.
enum Direction {
Up = 1,
Down,
Left,
Right,
}
Выше у нас числовое перечисление, где Up инициализируется значением 1. С этого момента все следующие члены автоматически увеличиваются. Другими словами, Direction.Up имеет значение 1, Down - 2, Left - 3 и Right - 4. При желании мы можем полностью отказаться от инициализаторов:
enum Direction {
Up,
Down,
Left,
Right,
}
Здесь Up будет иметь значение 0, Down - 1 и т. д. Такое поведение автоинкрементирования полезно в случаях, когда нам не важны сами значения членов перечисления, но важно, чтобы каждое значение отличалось от других значений в том же перечислении.
Использовать перечисления очень просто: просто обращайтесь к любому элементу как к свойству самого перечисления и объявляйте типы, используя имя перечисления:
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
respond("Princess Caroline", UserResponse.Yes);
Числовые перечисления могут содержать вычисляемые и константные элементы. Короткая версия состоит в том, что перечисления без инициализаторов либо должны быть первыми, либо должны следовать за числовыми перечислениями, инициализированными числовыми константами или другими константными элементами перечисления. Другими словами, следующее недопустимо:
enum E {
A = getSomeValue(),
B,
// Элемент Enum должен иметь инициализатор.
}
Строковые перечисления
Строковые перечисления представляют собой схожую концепцию, но имеют некоторые тонкие различия во время выполнения, которые описаны ниже. В строковом перечислении каждый элемент должен быть инициализирован строковым литералом или другим элементом строкового перечисления.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
Хотя строковые перечисления не имеют автоинкрементного поведения, преимущество строковых перечислений в том, что они хорошо "сериализуются". Другими словами, если вы занимаетесь отладкой и вам нужно прочитать значение числового перечисления во время выполнения программы, то это значение часто бывает непрозрачным - оно не передает никакого полезного смысла само по себе (хотя обратное отображение часто может помочь). Строковые перечисления позволяют вам давать значимое и читаемое значение, когда ваш код работает, независимо от имени самого элемента перечисления.
Смешанные перечисления
В техническом плане перечисления могут быть смешаны со строковыми и числовыми элементами:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
Если вы не пытаетесь использовать поведение JavaScript во время выполнения в своих целях, рекомендуется этого не делать.
Вычисляемые и константные элементы
Каждый элемент перечисления имеет связанное с ним значение, которое может быть либо константным, либо вычисляемым. Элемент перечисления считается константным, если:
- Он является первым элементом в перечислении и не имеет инициализатора, в этом случае ему присваивается значение 0:
tsx // E.X является константным: enum E { X, }
- Он не имеет инициализатора, и предыдущий элемент перечисления был числовой константой. В этом случае значение текущего элемента перечисления будет значением предыдущего элемента перечисления плюс один.
tsx // Все элементы перечисления в 'E1' и 'E2' являются константными enum E1 { X, Y, Z, } enum E2 { A = 1, B, C, }
- Элемент перечисления инициализирован выражением константного перечисления. Выражение константного перечисления - это подмножество выражений TypeScript, которые могут быть полностью вычислены во время компиляции. Выражение является выражением константного перечисления, если:
- Буквальное выражение перечисления (в основном строковый или числовой литерал)
- Ссылка на ранее определенный константный элемент перечисления (который может происходить из другого перечисления)
- Заключенное в скобки выражение константного перечисления
- Один из унарных операторов +, -, ~ с операндом-выражением константного перечисления
+, -, *, /, %, <<, >>, >>>, &, |, ^ бинарные операторы с операндами-выражениями константного перечисления
Это ошибка времени компиляции, когда выражения константных перечислений вычисляются в NaN или Infinity.
Во всех остальных случаях элемент перечисления считается вычисляемым.
enum FileAccess {
// константные элементы
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// вычисляемые элементы
G = "123".length,
}
Объединенные перечисления и типы элементов перечисления
Существует особое подмножество константных элементов перечисления, которые не вычисляются: литеральные элементы перечисления. Литеральный элемент перечисления - это константный элемент перечисления без инициализированного значения или с значениями, инициализированными:
- Любым строковым литералом (например, "foo", "bar", "baz")
- любым числовым литералом (например, 1, 100)
- унарным минусом, применённым к любому числовому литералу (например, -1, -100)
Когда все элементы перечисления имеют литеральные значения, в дело вступает особая семантика.
Первое - это то, что элементы перечисления также становятся типами. Например, мы можем сказать, что определенные элементы могут иметь только значение элемента перечисления:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// Тип 'ShapeKind.Square' не может быть присвоен
// типу 'ShapeKind.Circle'.
radius: 100,
};
Другое изменение заключается в том, что сами типы перечислений эффективно становятся объединением каждого элемента перечисления. С объединенными перечислениями система типов может использовать тот факт, что она знает точный набор значений, которые существуют в самом перечислении. Благодаря этому TypeScript может перехватывать ошибки, когда мы, возможно, сравниваем значения неправильно. Например:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// Это сравнение кажется непреднамеренным
// потому что типы 'E.Foo' и 'E.Bar' не пересекаются.
}
}
В этом примере мы сначала проверили, является ли x не E.Foo. Если эта проверка пройдет, тогда наше || сократится, и тело 'if' будет выполняться. Однако, если проверка не удалась, тогда x может быть только E.Foo, поэтому не имеет смысла проверять, не равен ли он E.Bar.
Перечисления во время выполнения
Перечисления являются реальными объектами, существующими во время выполнения. Например, следующее перечисление
enum E {
X,
Y,
Z,
}
можно передавать в функции
enum E {
X,
Y,
Z,
}
function f(obj: { X: number }) {
return obj.X;
}
// Работает, так как 'E' имеет свойство
// с именем 'X', которое является числом.
f(E);
Перечисления во время компиляции
Хотя перечисления являются реальными объектами, существующими во время выполнения, ключевое слово keyof работает иначе, чем можно было бы ожидать для обычных объектов. Вместо этого используйте keyof typeof для получения типа, представляющего все ключи перечисления в виде строк.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* Это эквивалентно:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log("Ключ уровня журнала:", key);
console.log("Значение уровня журнала:", num);
console.log("Сообщение уровня журнала:", message);
}
}
printImportant("ERROR", "Это сообщение");
Обратное преобразование
Помимо создания объекта со свойствами для элементов, числовые перечисления также получают обратное преобразование от значений перечисления к именам перечисления. Например, в этом примере:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
TypeScript компилирует это в следующий JavaScript:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
В этом сгенерированном коде перечисление компилируется в объект, который хранит как прямые (имя -> значение), так и обратные (значение -> имя) преобразование. Ссылки на другие элементы перечисления всегда выводятся как обращения к свойствам и никогда не встраиваются.
Обратите внимание, что для строковых элементов перечисления обратное преобразование вообще не генерируется.
const-перечисления
В большинстве случаев перечисления являются вполне подходящим решением. Однако иногда требования более жесткие. Чтобы избежать платы за дополнительный сгенерированный код и дополнительные непрямые обращения при доступе к значениям перечисления, можно использовать const-перечисления. Const-перечисления определяются с помощью модификатора const:
const enum Enum {
A = 1,
B = A * 2,
}
Const-перечисления могут использовать только выражения константных перечислений и, в отличие от обычных перечислений, они полностью удаляются во время компиляции. Элементы const-перечисления встраиваются в места использования. Это возможно, поскольку const-перечисления не могут иметь вычисляемых элементов.
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
в сгенерированном коде станет
"use strict";
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */,
];
Окружающие перечисления
Окружающие перечисления используются для описания формы уже существующих типов перечислений.
declare enum Enum {
A = 1,
B,
C = 2,
}
Одно важное отличие между окружающим и не окружающим перечислениями в том, что в обычных перечислениях элементы, не имеющие инициализатора, считаются константными элементами. Для элемента не константного окружающего перечисления, не имеющего инициализатора, элемент считается вычисляемым.
Объекты против перечислений
В современном TypeScript вам может не понадобиться перечисление, когда достаточно будет объекта с параметром as const:
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
EDirection.Up;
(enum member) EDirection.Up = 0
ODirection.Up;
(property) Up: 0
// Использование перечисления в качестве параметра
function walk(dir: EDirection) {}
// Для извлечения значений требуется дополнительная строка
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right);
Главный аргумент в пользу этого формата по сравнению с перечислениями TypeScript заключается в том, что он держит ваш код в гармонии с состоянием JavaScript, и когда/если перечисления будут добавлены в JavaScript, вы сможете перейти к дополнительному синтаксису.
Карта развития разработчика
Получите полную карту развития разработчика по всем направлениям: frontend, backend, devops, mobile