Menu

Enum в TypeScript: числовые, строковые и const enum

Enum в TypeScript это именованный набор констант, например enum Direction { Up, Down }. Числовые и строковые перечисления, JavaScript, в который компилируется enum, обратное отображение, перебор enum, const enum и когда лучше выбрать объединение строковых литералов или объект с as const.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Enum в TypeScript это именованный набор констант. enum Direction { Up, Down, Left, Right } создаёт и тип Direction, и объект во время выполнения, к членам которого обращаются как Direction.Up. Члены нумеруются с 0, если вы не задали им значения, а строковые перечисления дают каждому члену читаемую строку.

Enum одна из немногих возможностей TypeScript, которые не являются просто типами: при компиляции enum становится настоящим объектом JavaScript.

Числовые enum

Без инициализаторов члены получают 0, 1, 2 и так далее. Задайте первому члену число, и остальные продолжат от него. Можно также явно задать каждое значение; это безопасный выбор, когда числа хранятся в базе данных или передаются по сети.

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

Во что компилируется enum

Типы стираются, а enum нет. Вот JavaScript, который TypeScript генерирует для числового и строкового enum:

enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Down"] = 1] = "Down";
    Direction[Direction["Left"] = 2] = "Left";
    Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
    Status["Active"] = "ACTIVE";
    Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));

Direction["Up"] = 0 возвращает 0, поэтому в том же выражении задаётся Direction[0] = "Up". Значит, числовой enum отображает в обе стороны: имя в число и число обратно в имя. Это и есть обратное отображение. Строковые enum отображают только имена в значения.

У напечатанного объекта Direction восемь ключей: четыре имени и четыре числа. Это становится важно, как только вы начинаете его перебирать.

Строковые enum

Каждому члену строкового enum нужно явное строковое значение. Значения попадают в логи, JSON и базы данных как есть, поэтому строковые enum отлаживать проще, чем числа.

Строковый enum в одном отношении номинален, и это удивляет: обычную строку нельзя ему присвоить, даже если текст совпадает со значением члена.

index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?

(Подсказка в сообщении это догадка компилятора, и здесь она неверна; исправление: Status.Active.) В обратную сторону значение Status можно использовать везде, где ожидается string. Когда значения приходят строками, из JSON или формы, преобразуйте их с проверкой, как в разделе о проверке значений ниже.

Enum как тип

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

Если в Shape добавить новый член без нового case, sides перестанет компилироваться с ошибкой TS2366, Function lacks ending return statement and return type does not include 'undefined'. На странице switch показана более строгая проверка полноты через never.

Последние строки показывают реальную слабость числовых enum. Числовой литерал, который не совпадает ни с одним членом, const level: Level = 99, даёт ошибку компиляции (TS2322), но любое значение типа number принимается, поэтому 57 проходит. У строковых enum такой дыры нет.

Перебор enum

Во время выполнения enum это объект, поэтому Object.keys, Object.values и Object.entries работают. Для строкового enum они возвращают ровно его члены. Для числового они возвращают ещё и записи обратного отображения, которые нужно отфильтровать:

Чтобы типизировать переменную как «одно из имён членов enum», используйте keyof typeof Direction: это объединение "Up" | "Down" | "Left" | "Right". Тогда Direction[name] найдёт значение с полной типобезопасностью.

У строкового enum обратного отображения нет, поэтому, чтобы получить имя члена по значению, ищите в записях: Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0] даёт "Active" или undefined, если ни у одного члена такого значения нет.

Проверка, входит ли значение в enum

Данные извне программы это обычные string или number. Защитник типа сверяет их со значениями enum и сужает до типа enum:

Не пишите raw as Status для непроверенного ввода: утверждение компилируется, но во время выполнения ничего не проверяется, и "DELETED" пойдёт по программе с типом корректного Status.

const enum

const enum просит компилятор удалить enum и подставить значение каждого члена туда, где он используется. Объекта во время выполнения нет, поэтому ничего нельзя перебрать или обратно отобразить.

const enum экономит несколько байтов и одно обращение к свойству, но зависит от того, видит ли компилятор объявление enum при компиляции каждого файла, который его использует. Инструменты, транспилирующие по одному файлу, например Babel и swc, не видят const enum, объявленный в другом файле; удаление типов в Node отвергает const enum, как и любой другой enum; а при isolatedModules или verbatimModuleSyntax TypeScript сообщает об ошибке TS2748, если вы используете const enum из файла объявлений. Большинству прикладного кода const enum не нужны.

Enum, объединение типов или объект с as const

Есть три распространённых способа задать фиксированный набор значений:

enumОбъединение литераловОбъект с as const
Существует во время выполненияда, объектнетда, обычный объект
Перебор значенийObject.values (числовой: с фильтром)нет, перебирать нечегоObject.values
Принимает обычную строку "red"нет (строковые enum)дада
Доступ по имени X.Redданетда
Обратное отображениетолько числовые enumнетнет
Работает с удалением типов в Nodeнетдада
Разрешено при erasableSyntaxOnlyнетдада
Дополнительный синтаксисправила enum, const enumникакогоприём с typeof

Многие команды сейчас по умолчанию используют объединение строковых литералов и переходят на объект с as const, когда значения нужны во время выполнения (чтобы их перебрать или построить выпадающий список). Причины: объединения это чистые типы и исчезают из результата; они принимают обычные строки, которые приходят из JSON и API; а enum единственная часть повседневного TypeScript, которая не укладывается в формулу «JavaScript плюс стираемые типы».

Последний пункт стал практическим. Node запускает файлы .ts напрямую, удаляя типы, а enum удалить нельзя:

node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

Флаг Node --experimental-transform-types позволяет запускать enum, а опция компилятора erasableSyntaxOnly отмечает каждый enum ошибкой TS1294, This syntax is not allowed when 'erasableSyntaxOnly' is enabled., так что проект может запретить их заранее. Как работает удаление типов, описано на странице запуск TypeScript. Всё это не делает enum неправильными: код, скомпилированный tsc или бандлером, выполняет их без проблем, а кодовая база, где enum уже используются, мало что выиграет от их замены.

Часто задаваемые вопросы

Что такое enum в TypeScript?

Именованный набор констант, который одновременно является типом и объектом во время выполнения: enum Direction { Up, Down } позволяет писать Direction.Up и использовать Direction как тип параметра. В отличие от большинства возможностей TypeScript, enum не стирается: он компилируется в объект JavaScript, который существует во время выполнения.

Как перебрать enum в TypeScript?

Для строкового enum Object.values(MyEnum) даёт значения, а Object.keys(MyEnum) имена. Числовой enum содержит ещё и записи обратного отображения ("0": "Up"), поэтому их нужно отфильтровать: Object.keys(Direction).filter((k) => isNaN(Number(k))) даёт только имена. const enum перебрать нельзя, потому что во время выполнения его не существует.

Как преобразовать строку в значение enum в TypeScript?

Сверьте строку со значениями enum в защитнике типа: function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }. После проверки s имеет тип Status. Простое s as Status компилируется, но во время выполнения ничего не проверяет.

Что использовать в TypeScript: enum или объединение типов?

Многие команды предпочитают объединение строковых литералов (type Status = "active" | "inactive") или объект с as const, если значения нужны и во время выполнения. Объединения стираются полностью, работают со встроенным удалением типов в Node и опцией erasableSyntaxOnly и принимают обычные строки вроде "active". Enum тоже нормальный выбор, особенно в кодовых базах, где они уже используются.

Чем enum отличается от const enum?

Обычный enum компилируется в объект, который можно перебирать и в котором можно искать во время выполнения. const enum удаляется при компиляции, и каждое использование заменяется значением (Size.Large превращается в 2), поэтому во время выполнения он ничего не стоит, но его нельзя перебрать, а инструменты, компилирующие по одному файлу, его ограничивают.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ