Перечисления
Перечисления — это одна из немногих функций 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 Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
} Хотя строковые перечисления не имеют поведения автоматического инкрементирования, они обладают тем преимуществом, что хорошо «сериализуются». Другими словами, если вам нужно отлаживать и просматривать значение числового перечисления во время выполнения, это значение часто является неинформативным — оно само по себе не несет полезного смысла (хотя обратное отображение часто может помочь). Строковые перечисления позволяют задавать осмысленное и удобочитаемое значение во время работы вашего кода, независимо от имени самого члена перечисления.
Гетерогенные перечисления
Технически перечисления могут быть смешаны со строковыми и числовыми членами, но неясно, зачем это нужно:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
} Если вы не пытаетесь каким-то хитроумным способом использовать поведение JavaScript во время выполнения, не делайте этого.
Вычисляемые и константные члены
Каждый член перечисления имеет связанное с ним значение, которое может быть либо константным, либо вычисляемым. Член перечисления считается константным, если:
-
Он является первым членом перечисления и не имеет инициализатора, в этом случае ему присваивается значение
0.// E.X is constant: enum E { X, } -
У него нет инициализатора, а предыдущий член перечисления был числовой константой. В этом случае значение текущего члена перечисления будет равно значению предыдущего члена плюс единица.
// All enum members in 'E1' and 'E2' are constant. enum E1 { X, Y, Z, } enum E2 { A = 1, B, C, } -
Член перечисления инициализируется константным выражением перечисления. Константное выражение перечисления — это подмножество выражений TypeScript, которые можно полностью оценить во время компиляции. Выражение является константным выражением перечисления, если:
- это литеральное выражение перечисления (в основном строковая или числовая литераль)
- ссылка на ранее определенный константный член перечисления (который может происходить из другого перечисления)
- выражение константного перечисления в скобках
- один из
+,-,~унарных операторов, примененных к константному выражению перечисления -
+,-,*,/,%,<<,>>,>>>,&,|,^бинарные операторы с константными выражениями перечисления в качестве операндов
Константные выражения перечисления при вычислении не должны быть равны
NaNилиInfinity. Это ошибка компиляции.
Во всех остальных случаях член перечисления считается вычисляемым.
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
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,
radius: 100,
}; Другое изменение заключается в том, что сами типы перечислений фактически становятся объединением каждого члена перечисления. С объединенными перечислениями система типов может использовать тот факт, что она знает точный набор значений, существующих в самом перечислении. Благодаря этому TypeScript может обнаружить ошибки, связанные с некорректным сравнением значений. Например:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== 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;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E); Перечисления во время компиляции
Несмотря на то, что перечисления — это реальные объекты, существующие во время выполнения, ключевое слово keyof работает иначе, чем вы ожидаете для типичных объектов. Вместо этого используйте keyof typeof для получения типа, представляющего все ключи перечисления в виде строк.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* This is equivalent to:
* 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("Log level key is:", key);
console.log("Log level value is:", num);
console.log("Log level message is:", message);
}
}
printImportant("ERROR", "This is a message"); Обратные отображения
Помимо создания объекта с именами свойств для членов, члены числовых перечислений также получают обратное отображение от значений перечисления к именам перечисления. Например, в этом примере:
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"
В этом сгенерированном коде перечисление компилируется в объект, который хранит как прямое (name -> value ), так и обратное (value -> name) отображения. Ссылки на другие члены перечисления всегда генерируются как обращение к свойствам и никогда не встраиваются.
Обратите внимание, что члены строковых перечислений не получают сгенерированное обратное отображение.
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 */,
];
Недостатки Const-перечислений
Встраивание значений перечислений на первый взгляд простое, но имеет неявные последствия. Эти недостатки относятся только к глобальным Const-перечислениям (по сути, Const-перечислениям в файлах .d.ts ) и совместному использованию между проектами, но если вы публикуете или используете файлы .d.ts , эти недостатки, вероятно, применимы к вам, поскольку tsc --declaration преобразует файлы .ts в файлы .d.ts.
- По причинам, изложенным в
isolatedModulesдокументации, этот режим фундаментально несовместим с глобальными Const-перечислениями. Это означает, что если вы публикуете глобальные Const-перечисления, потребители по нижнему уровню не смогут использоватьisolatedModulesи эти значения перечисления одновременно. - Вы можете легко встроить значения из версии A зависимости во время компиляции и импортировать версию B во время выполнения. Перечисления версий A и B могут иметь разные значения, что, если вы не очень внимательны, может привести к непредвиденным ошибкам, например, при выборе неправильных ветвей
ifоператоров. Эти ошибки особенно коварны, потому что часто автоматизированные тесты запускаются примерно в то же время, что и сборка проектов, с одними и теми же версиями зависимостей, что полностью скрывает эти ошибки. -
importsNotUsedAsValues: "preserve"не будет элидировать импорты для Const-перечислений, используемых в качестве значений, но глобальные Const-перечисления не гарантируют существования файлов.jsво время выполнения. Неразрешимые импорты вызывают ошибки во время выполнения. Обычный способ однозначного элидирования импортов, импорты только типов, в настоящее время не поддерживают значения Const-перечислений.
Вот два подхода к предотвращению этих проблем:
A. Не используйте const перечисления вообще. Вы можете легко запретить const перечисления с помощью линтера. Очевидно, это устраняет любые проблемы с const перечислениями, но предотвращает инлайнирование собственных перечислений вашего проекта. В отличие от инлайнирования перечислений из других проектов, инлайнирование собственных перечислений проекта не проблематично и не имеет последствий для производительности. B. Не публикуйте глобальные const перечисления, деконструировав их с помощью preserveConstEnums. Это подход, принятый внутри проекта TypeScript. preserveConstEnums генерирует для const перечислений тот же JavaScript, что и для обычных перечислений. Затем вы можете безопасно убрать модификатор const из файлов .d.ts на стадии сборки.
Таким образом, потребители по нижней цепочке не будут инлайнировать перечисления из вашего проекта, избегая вышеупомянутых проблем, но проект по-прежнему может инлайнировать свои собственные перечисления, в отличие от полного запрета const перечислений.
Глобальные перечисления
Глобальные перечисления используются для описания структуры уже существующих типов перечислений.
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;
ODirection.Up;
// Using the enum as a parameter
function walk(dir: EDirection) {}
// It requires an extra line to pull out the values
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right); Главный аргумент в пользу этого формата по сравнению с enum перечислениями TypeScript заключается в том, что он поддерживает вашу кодовую базу в соответствии с состоянием JavaScript, и при/если перечисления будут добавлены в JavaScript, вы сможете перейти к дополнительному синтаксису.
© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/enums.html