TypeScript здорового человека, или почему с Enum лучше

Наверное, большинство фронтенд-разработчиков в какой-то момент сталкивались с задачей внедрения 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 ругнется, когда мы попробуем создать бухгалтера:

0db2eeedddfce22ed5bd2e23db4a6e48.png

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

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

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

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

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

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

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

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

f947dbedce3744c33ef7e7b15efff0c4.png

Потому что теперь строка «Директор» это просто какая-то строка, не имеющая отношения к Перечислению 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(
  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, мы можем точно получить тип пользователя, с которым имеем дело в каждом конкретном случае.

7fbb6d071006b7db4cd45df2cb9d84ea.png78de7a94f841aa340979dbc4ad8bfaca.png

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

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

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

a4c0838378260aad65edcf371600e3bb.png

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

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

© Habrahabr.ru