Перейти к содержимому
Теория · Объединения и литеральные типы · урок 7 из 8

Перечисления enum

В лифте бизнес-центра на кнопках написано «Парковка», «Холл» и «Крыша». Человек нажимает кнопку по названию, а лифт получает номер этажа: 0, 1 или 2. Названия удобны людям, номера — машине, а кнопки связывают одно с другим.

В программах такие наборы встречаются постоянно: уровни сложности, статусы заказа, роли пользователей. Можно писать в коде голые числа 0, 1 и 2, но через месяц никто не вспомнит, что значит 2. В TypeScript для именованных наборов значений есть перечисление — его объявляют словом enum.

TypeScript
enum Level {
    Low,
    Mid,
    High,
}

function describe(level: Level) {
    console.log(level);
}

describe(Level.Mid);  // 1
describe(Level.High); // 2

Давайте разберём пример по строкам:

  1. enum Level { … } объявляет перечисление с тремя членами: Low, Mid и High.
  2. Члены получают номера по порядку, начиная с нуля: Low — 0, Mid — 1, High — 2.
  3. level: Level — имя перечисления работает как тип: параметр принимает члены Level, а describe(7) tsc отклонит: среди значений Level нет 7. Число, равное значению члена, например describe(1), tsc пропустит.
  4. describe(Level.Mid) печатает 1, а не Mid: значение члена — число.

Значения членов

Здесь аналогия с лифтом заканчивается. Номера кнопок выбрал завод, а значения членов enum можно задать самому — и числа, и строки.

TypeScript
enum Priority {
    Low = 1,
    Mid,
    High,
}

enum Color {
    Red = "RED",
    Green = "GREEN",
}

console.log(Priority.Mid, Priority.High); // 2 3
console.log(Color.Green);                 // GREEN

Свои числа

Low = 1 задаёт первому члену значение вручную. Член без значения получает значение предыдущего плюс один, поэтому Mid — 2, а High — 3. Это правило работает в любом месте перечисления, а не только в начале.

Строковые значения

Red = "RED" делает перечисление строковым. Такие значения удобно читать в логах и в данных с сервера. Счёт от предыдущего для строк не работает, поэтому значение пишут у каждого члена — иначе tsc сообщит Enum member must have initializer.

enum остаётся в JavaScript

Аннотации и объявления type tsc стирает. С enum так не выходит: его значения нужны при запуске. Вот начало .js-файла, который tsc пишет из первого примера, — Level стал настоящим объектом:

JavaScript
"use strict";
var Level;
(function (Level) {
    Level[Level["Low"] = 0] = "Low";
    Level[Level["Mid"] = 1] = "Mid";
    Level[Level["High"] = 2] = "High";
})(Level || (Level = {}));

Каждая строка внутри функции записывает пару в обе стороны: Level.Mid даёт 1, а Level[1] даёт "Mid". Так у числового перечисления можно узнать имя члена по значению.

Этот вопрос часто задают на собеседовании: «Что остаётся от enum после компиляции?» Ответ: объект с членами, а у числового enum — ещё и обратные пары «номер → имя».

Объединение литералов вместо enum

Многие команды вместо enum пишут объединение литеральных типов: type Level = "low" | "mid" | "high";. Оно проверяется так же строго, но целиком исчезает при компиляции и не добавляет в .js ни строчки. enum выбирают, когда нужны имена для чисел или объект со всеми значениями при запуске.

Частая ошибка

Файл с enum запускают через node levels.ts:

TypeScript
enum Level { // ✗
    Low,
    Mid,
    High,
}

console.log(Level.Mid);
TEXT
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

tsc этот файл компилирует без ошибок. Отказывает Node: node file.ts работает в режиме, который только стирает типы, а enum нужно превратить в объект. Скомпилируйте файл через tsc levels.ts и запустите node levels.js или проверьте код в TypeScript Playground. Объединение литералов node file.ts запускает без проблем.

Главное

  • enum даёт имена набору значений; нумерация идёт с 0, а член без значения получает значение предыдущего плюс один.
  • У строкового enum значение пишут у каждого члена.
  • enum не стирается: tsc превращает его в объект в .js, а node file.ts такой файл не запускает.
  • Объединение литералов "low" | "mid" | "high" — частая замена enum: оно целиком исчезает при компиляции.