Наверное, большинство фронтенд-разработчиков в какой-то момент сталкивались с задачей внедрения TypeScript на проект. Обычно это задача выполняется не сразу, а постепенно. Сначала просто переименовываются все файлы из .js в .ts с проставлением везде типа "any", просто чтобы проект запустился, и только потом постепенно разработчики начинают заниматься планомерным переводом.

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

Зачастую, при создании типов и интерфейсов, описывая какое-то свойство, которое может принимать определенное, конечное количество строковых значений, разработчики указывают тип поля "string" или в крайнем случае перечисляют эти значения через "или".

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

interface Person {
    name: string;
    age: number;
    position: string;
  }

Ошибок нет. Вроде бы все работает, однако какие проблемы это может создать? Если имя - это строка, которая может принимать любое значение, то должность в компании - это тоже строка, но принимать она может только вполне определенное и конечное количество строковых значений. Например, в нашей компании есть только директор и продавец. В случае, если мы попытаемся создать объект с должностью "бухгалтер", такой тип ошибки не выдаст:

const person: Person = {
  name: 'Иван',
  age: 35,
  position: 'Бухгалтер'
}

 Самый простой и быстрый (но неправильный) способ решить эту проблему - создать условный тип и перечислить в типе все возможные значения:

  type Position = 'Директор' | 'Продавец';

  interface Person {
    name: string;
    age: number;
    position: Position;
  }

Тогда умный TypeScript ругнется, когда мы попробуем создать бухгалтера:

И вроде бы проблема решена, но нет.

И, как вы наверно поняли из названия статьи, все эти проблемы можно решить, используя такую замечательную часть TypeScript, как Перечисления (Enum).

Согласно документации, Enum, это перечисления, которые позволяют разработчику определить набор именованных констант.

TypeScript предоставляет как числовые, так и строковые перечисления. В данной статье речь пойдет именно о строковых Enums.

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

  enum Position {
    Director = 'Директор',
    Seller = 'Продавец'
  }

  interface Person {
    name: string;
    age: number;
    position: Position;
  }

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

Потому что теперь строка «Директор» это просто какая-то строка, не имеющая отношения к Перечислению Position.

Теперь должность везде мы указываем вот так:

const person: Person = {
    name: 'Иван',
    age: 35,
    position: Position.Director
  }

И если должность «Директор» у нас в фирме изменится на «Генеральный Директор», то изменение необходимо будет ввести лишь в одном месте – Enum.

enum Position {
  Director = 'Генеральный директор',
  Seller = 'Продавец'
}

Рассмотрим два случая, когда использование Enum дает нам интересные дополнительные преимущества, помимо хорошей структуризации кода.

1. Работа с Enum, как с интерфейсами.

Допустим нам необходимо разделить сотрудников организации по должностям. Например, пусть будет интерфейс сотрудника Director и интерфейс сотрудника Seller.

interface Director {
  position: Position.Director;
  name: string;
  salary: number;
};

interface Seller {
  position: Position.Seller;
  name: string;
  salary: number;
  product: string;
}

Как и раньше, у них есть поле position, которое определено через enum. Давайте напишем функцию, которая будет принимать на вход сотрудника любого из этих двух типов и, в зависимости от значения поля position, возвращать данного сотрудника с одним из указанных типов.

function employeeTypeChecker<T extends Position>(
  position: T, employee: Director | Seller 
) {
  if (position === Position.Director) {
    return employee as T extends Position.Director ? Director : never
  } else {
    return employee as T extends Position.Seller ? Seller : never;    
  }
}

Теперь давайте создадим двух пользователей с неизвестным типом, но с точно определенным полем position.

const user1 = {
  position: Position.Seller as const,
  name: 'Mary',
  salary: 5000,
  product: 'Phone'
} 

const user2 = {
  position: Position.Director as const,
  name: 'John',
  salary: 10000,
} 

Обратите внимание, что у наших пользователей должность может принимать только одно из возможных значений Enum Position. И теперь, с помощью employeeTypeChecker, мы можем точно получить тип пользователя, с которым имеем дело в каждом конкретном случае.

Это стало возможным благодаря тому, что в функции employeeTypeChecker мы работаем с Enum как с интерфейсом. Мы можем применять extends, можем использовать условные типы. Если бы поле position было строкой, такое было бы невозможно.

 2. Перевод enum в массив

Еще один полезный кейс, который нам дает Enum - это легкий способ получения массива всех его возможных значений. Так как Enum по своей сути это объект, то применение Object.values(Enum), дает нам массив строковых значений Enum.

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

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

Комментарии (21)


  1. azizoid
    10.04.2023 23:21

    Если мне не изменяет память то создатель TS жаловался на enum. Я полез в гугл чтобы найти ту статью, но поиск выдал очень много статей
    Как говорится: отрицаешь - предлагай:
    Вместо enum можно использовать объекты
    const Position = { Director: 'Генеральный директор', Seller: 'Продавец' } as const


    1. doomguy49
      10.04.2023 23:21
      +2

      А вместо объектов можно использовать строковые и числовые переменные, только зачем?

      Жалуетесь на енамы, при этом причины не приводите, так может быть с ними все хорошо и не надо колдовать?


      1. Ekseft
        10.04.2023 23:21
        +1

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

        В документации TS есть аргумент в пользу объектов как раз из-за совместимости.


    1. Bono_houdini
      10.04.2023 23:21
      +3

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

      function foo(position: Position) {
      
      }


      1. SWATOPLUS
        10.04.2023 23:21
        +1

        type Position = typeof Positions[keyof typeof Positions];

        Сделать обёртку над двойным typeof оставляю в качестве домашнего задания.


        1. Bono_houdini
          10.04.2023 23:21
          +3

          Создавать объект на основе которого создавать тип выглядит овер когда для этого можно использовать только енум


        1. dom1n1k
          10.04.2023 23:21
          +3

          Честно говоря, выглядит как идеальная иллюстрация к понятию "в гамаке и стоя".


    1. iliazeus
      10.04.2023 23:21
      +2

      Если мне не изменяет память то создатель TS жаловался на enum.

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


      1. iliazeus
        10.04.2023 23:21
        +1

        В том числе, с enum можо зачем-то делать вот такую магию.


      1. Bono_houdini
        10.04.2023 23:21
        +1

        Этот нюанс решается с помощью const enum


    1. crypt0grapher
      10.04.2023 23:21
      +1

      Это очень хороший комментарий.
      Два прекрасных подхода к перечислениям, - enum и as const
      Enum, конечно, самый универсальный, являясь и значением и типом.
      Если объявлять объектом
      `const Positions = { Director: 'Генеральный директор', Seller: 'Продавец' } as const;`
      То для типа не обойтись без чего-то вроде
      `typePositions = (typeof Positions)[keyof typeof Positions];`
      (что абсолютно то же самое, что 'Директор' | 'Продавец')

      Но тут есть хорошая особенность! Что для одной задачи недостаток (когда позиции надо строго зафиксировать в enum), а для другой преимущество (когда необходимо работать с внешними текстами).

      Дело в том, что при enum нельзя присвоить const seller: Positions.Seller = 'Продавец';
      Только из enum, будьте добры. С объектом - ok.
      Такая штука.


    1. Fundorin174 Автор
      10.04.2023 23:21

      Так enum это и есть обёртка над объектами. Так же можно сказать что вместо async await можно использовать promise. Никто не спорит. Вкусовщина.


  1. Gary_Ihar
    10.04.2023 23:21
    +2

    Самый простой и быстрый (но неправильный) способ решить эту проблему

    И вроде бы проблема решена, но нет.

    Расскажите пожалуйста почему неправильно и почему проблема не решена ?


    1. Fundorin174 Автор
      10.04.2023 23:21

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


      1. dimoff66
        10.04.2023 23:21
        -1

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

        const getPositionView = (pos: Position) => pos === 'бухгалтер'
        ? "Старший бухгалтер" 
        : pos === "Директор"
        ? "Младший директор" 
        : pos
        
        
        

        Проблема решена


      1. LEXA_JA
        10.04.2023 23:21

        Typescript умеет переименовывать константные строки, как и находить на них ссылки


      1. Gary_Ihar
        10.04.2023 23:21
        +1

        Принял, если проблема только в этом, то получается на вкус и цвет )

        Что касается меня, то:
        1) не нравится лишний импорт.
        2) напрягает, что оно компилится в настоящий JS код. В нынешнее время это конечно так себе аргумент, но это чисто моё "фе" )

        П.С. Не хочу с вами спорить, будет чисто сотрясание воздуха, но пример у вас неудачный) Старший бухгалтер не появится в экосистеме, где невозможен обычный бухгалтер. Так что вы просто расширите enum. А вот если пропадет "бухгалтер" и появится "грузчик" с такой же бизнесовой логикой(пример тоже кстати отстой, тут важна идея), то вам скорее всего так же придется переименовывать ваш enum, ну типа как так то, под переменной "accountant" скрывается "грузчик" ))


        1. Fundorin174 Автор
          10.04.2023 23:21

          По поводу неудачного примера согласен


  1. x4erem6a
    10.04.2023 23:21
    +1

    Самый простой и быстрый (но неправильный) способ решить эту проблему - создать условный тип и перечислить в типе все возможные значения:

    type Position = 'Директор' | 'Продавец';
    interface Person {
      name: string;
      age: number;
      position: Position;
    }

    Но ведь здесь нет условного типа (conditional type). Даже если вы не это имели в виду думаю что не стоит путать читателя.

    Работа с Enum, как с интерфейсами.

    Здесь и далее по текста - а при чём тут собственно интерфейсы? Почему "как с интерфейсами"?

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

    Ни интерфейсы, ни енамы здесь ни при чём. Приведённый вами employeeTypeChecker точно так же воспроизводится с "неправильными" литеральными типами.

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


  1. 350str
    10.04.2023 23:21

    сомнительные преимущества, при использовании тех же литеральных типов мы имеем отличный автокомплит в ide и не обязаны каждый раз импортировать enum, если нужно где-то использовать тип с ним внутри


  1. Hamlet_dat
    10.04.2023 23:21
    +1

    Блин... Что за система у вас такая, что при использовании фактически справочных элементов приходится контролировать соответствие значения элементу справочника?

    Каким образом туда может попасть не то значение?

    Несоответствие типа при ручном вводе еще можно понять, но несоответствие строки... Если нужно строгое соответствие, то не даёте вводить руками. Только select и т.п.

    Не могу представить ни одного сценария, когда описанное вообще имеет смысл... Эдакая сферическая в вакууме проблема.