Перечисления TypeScript вызывают много ненависти. И не по неоправданной причине: у них так много потенциальных ножных ружей.

Но — моя позиция такова, что они по-прежнему принадлежат языку, а не вне его. Как пресловутый врач, который советует пациенту, у которого болит рука, когда он двигает ею вот так, я возражу: просто не двигайте ею вот так.

Однако я считаю, что в будущей основной версии TypeScript должны быть удалены или устранены многие из этих антишаблонов.

1. Не полагайтесь на неявные значения

Один из способов написать перечисление в Typescript:

enum Suit {
  Hearts,
  Diamonds,
  Clubs,
  Spades
}

Это транспилируется во что-то похожее на:

const Suit = {
  Hearts: 0,
  Diamonds: 1,
  Clubs: 2,
  Spades: 3
}

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

2. Не определяйте явные числовые значения

Когда вы это сделаете, вы получите странный результат:

enum Suit {
  Hearts = 0,
  Diamonds = 1,
  Clubs = 2,
  Spades = 3
}

Транспилируется в:

const Suit = {
  Hearts: 0,
  Diamonds: 1,
  Clubs: 2,
  Spades: 3,
  0: 'Hearts',
  1: 'Diamonds',
  2: 'Clubs',
  3: 'Spades'
}

По какой-то причине этого не происходит, если вы используете строковые значения перечисления.

Я считаю, что это сделано для того, чтобы упростить обратный поиск ключа, например:

const heartsKey = Suit[Suit.Hearts];

Но это подводит нас к следующему совету:

3. Не выполняйте обратный поиск ключей перечисления

На самом деле нет веской причины делать такие вещи.

const heartsKey = Suit[Suit.Hearts];

Значения перечисления должны иметь значение для вашего кода. Имена ключей — это деталь реализации, и они служат только для того, чтобы сделать ваш код более читабельным.

Если вам нужно описательное имя, просто определите новое сопоставление значения перечисления с именем.

4. Не используйте числовые значения enum в функциях

Да, эти вещи действительно отстой.

enum Suit {
  Hearts = 0,
  Diamonds = 1,
  Clubs = 2,
  Spades = 3
}

function logSuit(suit : Suit) {
    console.log(suit);
}

logSuit(1337);

Это не вызывает ошибку типа. Так какой смысл вообще иметь перечисление в этот момент?

5. Не используйте литеральные значения, если тип перечисления

Один из распространенных аргументов против перечислений: «Я не могу передать буквальное значение, даже если оно принадлежит перечислению».

enum Suit {
  Hearts = 'hearts',
  Diamonds = 'diamonds',
  Clubs = 'clubs',
  Spades = 'spades'
}

function logSuit(suit : Suit) {
    console.log(suit);
}

logSuit('hearts');

// Argument of type '"hearts"' is not assignable to parameter of type 'Suit'.(2345)

Для меня это фича, а не баг. TypeScript рассматривает перечисления как «номинальные», то есть недостаточно просто использовать значение с правильным типом, вы фактически должны использовать конкретный именованный тип.

Я не уверен, почему я на самом деле хочу произвольные магические строки, плавающие вокруг моей кодовой базы, такие как «сердечки». Импорт перечисления намного чище и дает мне очевидную запись о том, где конкретно это перечисление используется в коде, и я могу использовать его для поиска по ссылке.

6. Не используйте объект вместо перечисления

Это обычный совет, но я его ненавижу.

Мы можем заменить перечисление литеральным объектом с as const, чтобы сохранить литеральные типы значений, чтобы они не преобразовывались в string:

const Suit = {
  Hearts: 'hearts',
  Diamonds: 'diamonds',
  Clubs: 'clubs',
  Spades: 'spades'
} as const

Затем мы можем извлечь тип enum-esque, выведя объединение значений этого объекта:

type SuitEnum = (typeof Suit)[keyof typeof Suit]

Я имею в виду, конечно, это работает, но:

  1. Он страдает от проблемы «Не используйте литеральные значения, когда тип — enum».
  2. Мы должны помнить, чтобы отметить Suit с as const. Если мы забудем это сделать, наш код неявно станет менее типобезопасным: SuitEnum будет просто string. Предупреждений о добавлении as const нет.
  3. Создание SuitEnum приводит к довольно многословному фрагменту кода, который легко испортить, и который enum уже делает в хорошем содержательном виде.

Так что для меня эта модель создает больше проблем, чем решает.

7. Не используйте объединение строковых литералов вместо перечисления

Другие люди рекомендуют такой шаблон:

type Suit = 'hearts' | 'diamonds' | 'clubs' | 'spades';

Это нормально, если вам не нравится фактическое перечисление, на которое можно ссылаться во время выполнения, и если вам нравится разбрасывать волшебные строки повсюду в вашем коде. Мне не нравится ни одна из этих вещей.

8. Не используйте константные перечисления

Я имею в виду, э... константные перечисления - это нормально.

const enum Suit {
  Hearts = 'hearts',
  Diamonds = 'diamonds',
  Clubs = 'clubs',
  Spades = 'spades'
}

function logSuit(suit : Suit) {
    console.log(suit);
}

logSuit(Suit.Clubs);

Что хорошо, так это то, что перечисление Suit полностью стирается во время компиляции, а это означает, что вокруг не висит какой-то странный объект. Вот что мы получаем:

function logSuit(suit) {
    console.log(suit);
}

logSuit("clubs");

Раздражает то, что перечисление Suit полностью стирается во время компиляции. Это отстой, если вам действительно нравится иметь полное перечисление, когда оно вам нужно.

Например, если вы хотите написать валидатор, который берет значение из ненадежного источника — скажем, как часть полезной нагрузки json, переданной в http API, — и проверяет, соответствует ли это значение типу перечисления. В этот момент вам понадобится исходный объект Suit, а с const enum вы его потеряли.

9. Не объявляйте перечисления-слияния

Изменяемые типы в TypeScript довольно неприятны, независимо от того, говорим мы о перечислениях или нет.

enum Suit {
  Hearts = 'hearts',
  Diamonds = 'diamonds',
  Clubs = 'clubs',
}

enum Suit {
  Spades = 'spades'
}

Просто… не делай этого. Не нужно.

Единственный «хороший» способ

Просто используйте enum, всегда указывайте явные значения и убедитесь, что значения являются строками:

enum Suit {
  Hearts = 'hearts',
  Diamonds = 'diamonds',
  Clubs = 'clubs',
  Spades = 'spades'
}

Да, это искусственное ограничение. Но это лучше, чем другие альтернативные искусственные ограничения, которые вам нужно использовать, чтобы обойти это.

Будущее

Будем надеяться, что будущая основная версия TypeScript сделает что-то вроде этого «единственным верным способом» для написания перечислений, а также заставит числовые значения перечислений вести себя разумно.

Они могли бы сделать это даже с настройкой "saneEnums": true в tsconfig.json.

Я также был бы полностью открыт для какого-либо подхода, который сделал бы использование «объектов как перечислений» менее громоздким. Что-то вроде:

const Suit = {
  Hearts: 'hearts',
  Diamonds: 'diamonds',
  Clubs: 'clubs',
  Spades: 'spades'
};

function logSuit(suit: valueof Suit) {
  console.log(suit);
}

Но до тех пор я буду придерживаться перечислений.