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

Перечисления в TypeScript предоставляют ряд преимуществ:

- Безопасность типов*: перечисления сами по себе являются типами, а это означает, что TypeScript может проверять правильность значений, присвоенных переменным перечисления.

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

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

Есть несколько особенностей перечислений в TypeScript, которые могут быть не столь очевидны:

  1. Обратное сопоставление: перечисления в TypeScript поддерживают обратное сопоставление, что означает, что вы можете получить значение перечисления из соответствующего строкового представления. Например:
enum Color {
  Red = 1,
  Green,
  Blue
}

const colorName = "Green";
const colorValue = Color[colorName]; // 2

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

2. Гетерогенные перечисления: TypeScript позволяет перечислениям иметь как строковые, так и числовые значения и даже значения разных типов в одном перечислении. Однако, как правило, это не рекомендуется, так как это может привести к путанице и неожиданному поведению. Например:

enum MyEnum{
  A = 1,
  B = "B",
  C = true
}

const a = MyEnum.A; // 1
const b = MyEnum.B; // "B"
const c = MyEnum.C; // true

Хотя это возможно, обычно лучше использовать перечисления с согласованными типами, чтобы избежать путаницы.

3. Перечисления как битовые флаги. Перечисления в TypeScript можно использовать как битовые флаги, где каждое значение перечисления представляет один бит в большем двоичном значении. Это может быть полезно в случаях, когда вам нужно представить несколько логических флагов в одном значении. Например:

enum Flags {
  None = 0,
  A = 1 << 0,
  B = 1 << 1,
  C = 1 << 2
}

const flagValue = Flags.A | Flags.C; // 5 (binary 101)
const hasFlagA = flagValue & Flags.A; // true
const hasFlagB = flagValue & Flags.B; // false
const hasFlagC = flagValue & Flags.C; // true

Этот подход может быть более эффективным, чем использование отдельных логических значений, но с ним может быть и сложнее работать.

Это некоторые из уникальных особенностей перечислений в TypeScript, и хотя они могут не использоваться повсеместно, в некоторых случаях они могут быть полезны.

Темная сторона перечислений

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

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

- Возможные конфликты имен: перечисления используют то же пространство имен, что и другие идентификаторы в TypeScript, а это означает, что существует вероятность конфликтов имен, если одно и то же имя используется как для перечисления, так и для другого идентификатора.

- Неуникальные значения: в TypeScript можно присвоить одно и то же значение нескольким членам перечисления. Это может вызвать путаницу и неожиданное поведение при использовании перечисления.

- Ограниченная расширяемость: после определения перечисления его нельзя расширить или изменить. Это может затруднить адаптацию перечисления к меняющимся требованиям или добавление к нему новых значений.

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

- Использование перечислений для числовых значений: перечисления не предназначены для использования в качестве замены числовых значений. Например, определение перечисления со значениями 0, 1, 2 и т. д. не является предполагаемым вариантом использования перечислений.

- Использование строковых значений в перечислениях: хотя TypeScript позволяет перечислениям использовать строковые значения, обычно это не рекомендуется. Строки менее эффективны, чем числовые значения, и могут быть более подвержены опечаткам и ошибкам.

- Перечисления не могут быть расширены

Когда код TypeScript компилируется в JavaScript, перечисления преобразуются в объекты со значениями перечислений в качестве свойств. Это может привести к нескольким потенциальным недостаткам.

Увеличенный размер файла. Перечисления могут добавлять дополнительный код в скомпилированный файл JavaScript, что может увеличить размер файла. Это может не быть серьезной проблемой для небольших перечислений, но более крупные перечисления могут добавить заметные накладные расходы.

Увеличение использования памяти. Перечисления в JavaScript — это объекты со свойствами, что означает, что они занимают больше памяти, чем простые значения, такие как числа или строки. Если у вас большое перечисление или вы часто используете перечисления, это может повлиять на производительность.

Значения перечисления не уникальны. В отличие от TypeScript, где значения перечисления гарантированно уникальны, в JavaScript значения перечисления могут быть перезаписаны или доступны под разными именами. Это может привести к неожиданному поведению, если не использовать его осторожно.

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

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

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

Несколько примеров неправильного использования перечислений:

  1. Чрезмерное использование перечислений:
enum DaysOfWeek {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

// In this example, an enum is used to define the days of the week. However, this enum is not necessary as the days of the week are already well-known and fixed. Using an enum in this case adds unnecessary complexity to the code
// Same goes to use string values for this enums. It's just redundant.

2. Использование перечислений для числовых значений:

enum Age {
  Zero = 0,
  One,
  Two,
  Three,
  Four
}

// In this example, an enum is used to define ages, but it is not appropriate as ages are typically represented by numeric values, not named constants.
// Also if this will be used as type it will allow values that exceed amount of values in enum

let myNumber: Age = 5; // won't cause any error, even if we have only 0,1,2,3,4 as allowed values.

Как заменить

В определенных ситуациях перечисления могут быть заменены другими функциями языка.

  1. Типы объединения: Типы объединения могут использоваться для представления набора именованных значений. Например, вместо определения перечисления для цветов вы можете определить тип объединения:
type Color = 'Red' | 'Green' | 'Blue';

let color: Color = 'Red';

2. Литералы объектов: вместо использования перечисления для представления набора именованных значений вы можете использовать литерал объекта:

const Color = {
  Red: '#FF0000',
  Green: '#00FF00',
  Blue: '#0000FF',
} as const;

type Color = typeof Color[keyof typeof Color];

let color: Color = 'Red';

3. Типы строковых литералов. Типы строковых литералов также могут использоваться для представления набора именованных значений. Например, вместо определения перечисления для кодов состояния HTTP вы можете определить тип строкового литерала:

type HttpStatusCode = '200' | '201' | '400' | '401' | '404' | '500';

let statusCode: HttpStatusCode = '200';

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

abstract class Animal {
  abstract name: string;
  abstract makeSound(): void;
}

class Dog extends Animal {
  name = 'Dog';
  makeSound() {
    console.log('Woof!');
  }
}

class Cat extends Animal {
  name = 'Cat';
  makeSound() {
    console.log('Meow!');
  }
}

let dog = new Dog();
dog.makeSound(); // logs "Woof!"

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

Используемые источники:

В Typescript есть объединения, значит, перечисления излишни?

TSDoc

Документация по TypeScript