Наверное, большинство фронтенд-разработчиков в какой-то момент сталкивались с задачей внедрения 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)
Gary_Ihar
10.04.2023 23:21+2Самый простой и быстрый (но неправильный) способ решить эту проблему
И вроде бы проблема решена, но нет.
Расскажите пожалуйста почему неправильно и почему проблема не решена ?
Fundorin174 Автор
10.04.2023 23:21Ну потому, что если через какое то время должность "бухгалтер" изменится на "старший бухгалтер" вам придётся менять все строки "бухгалтер" в коде. А найти их все будет ой как не просто
dimoff66
10.04.2023 23:21-1Во первых не понятно почему найти их будет непросто. Во вторых пример странный ибо это две разные должности. В третьих совершенно не факт что энум должен выводиться на экран. Но окей, допустим выводится и нужно изменить вывод. Для этого достаточно на всякий случай сделать малюсенькую функцию вывода и при выводе использовать её.
const getPositionView = (pos: Position) => pos === 'бухгалтер' ? "Старший бухгалтер" : pos === "Директор" ? "Младший директор" : pos
Проблема решена
LEXA_JA
10.04.2023 23:21Typescript умеет переименовывать константные строки, как и находить на них ссылки
Gary_Ihar
10.04.2023 23:21+1Принял, если проблема только в этом, то получается на вкус и цвет )
Что касается меня, то:
1) не нравится лишний импорт.
2) напрягает, что оно компилится в настоящий JS код. В нынешнее время это конечно так себе аргумент, но это чисто моё "фе" )
П.С. Не хочу с вами спорить, будет чисто сотрясание воздуха, но пример у вас неудачный) Старший бухгалтер не появится в экосистеме, где невозможен обычный бухгалтер. Так что вы просто расширите enum. А вот если пропадет "бухгалтер" и появится "грузчик" с такой же бизнесовой логикой(пример тоже кстати отстой, тут важна идея), то вам скорее всего так же придется переименовывать ваш enum, ну типа как так то, под переменной "accountant" скрывается "грузчик" ))
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
, да и в целом проговорить почему енамы являются темой для споров в сообществе. У них ведь есть заметные недостатки при всех их преимуществах.
350str
10.04.2023 23:21сомнительные преимущества, при использовании тех же литеральных типов мы имеем отличный автокомплит в ide и не обязаны каждый раз импортировать enum, если нужно где-то использовать тип с ним внутри
Hamlet_dat
10.04.2023 23:21+1Блин... Что за система у вас такая, что при использовании фактически справочных элементов приходится контролировать соответствие значения элементу справочника?
Каким образом туда может попасть не то значение?
Несоответствие типа при ручном вводе еще можно понять, но несоответствие строки... Если нужно строгое соответствие, то не даёте вводить руками. Только select и т.п.
Не могу представить ни одного сценария, когда описанное вообще имеет смысл... Эдакая сферическая в вакууме проблема.
azizoid
Если мне не изменяет память то создатель TS жаловался на enum. Я полез в гугл чтобы найти ту статью, но поиск выдал очень много статей
Как говорится: отрицаешь - предлагай:
Вместо enum можно использовать объекты
const Position = { Director: 'Генеральный директор', Seller: 'Продавец' } as const
doomguy49
А вместо объектов можно использовать строковые и числовые переменные, только зачем?
Жалуетесь на енамы, при этом причины не приводите, так может быть с ними все хорошо и не надо колдовать?
Ekseft
Причина заключается в том, что это не стандарт языка, а нечто, что привносит TypeScript в рантайм, в отличие от остальной типизации, которая просто удаляется во время траспиляции. И как и в случае с декораторами, когда данная конструкция появится в JavaScript, она может сильно отличаться от текущей реализации. Возможно если бы enum'ы были за экспериментальным флагом как и декораторы, больше людей обращало бы на это внимания.
В документации TS есть аргумент в пользу объектов как раз из-за совместимости.
Bono_houdini
Объект не может заменить енум тк его нельзя использовать в качестве типа. Элементарно нельзя сделать
SWATOPLUS
type Position = typeof Positions[keyof typeof Positions];
Сделать обёртку над двойным typeof оставляю в качестве домашнего задания.
Bono_houdini
Создавать объект на основе которого создавать тип выглядит овер когда для этого можно использовать только енум
dom1n1k
Честно говоря, выглядит как идеальная иллюстрация к понятию "в гамаке и стоя".
iliazeus
Насколько помню, это связано, в первую очередь с тем, что
enum
существует не только на уровне типов, а транслируется в не слишком очевидный код. Это противоречит современной философии TypeScript - добавлять фичи только на уровне типов, а если нужно что-то поменять в семантике времени исполнения - писать пропозалы для ECMAScript.iliazeus
В том числе, с
enum
можо зачем-то делать вот такую магию.Bono_houdini
Этот нюанс решается с помощью const enum
crypt0grapher
Это очень хороший комментарий.
Два прекрасных подхода к перечислениям, -
enum
иas const
Enum, конечно, самый универсальный, являясь и значением и типом.
Если объявлять объектом
`const Positions = { Director: 'Генеральный директор', Seller: 'Продавец' } as const;`
То для типа не обойтись без чего-то вроде
`
type
Positions= (typeof
Positions)[keyof typeof
Positions];`
(что абсолютно то же самое, что 'Директор' | 'Продавец')
Но тут есть хорошая особенность! Что для одной задачи недостаток (когда позиции надо строго зафиксировать в enum), а для другой преимущество (когда необходимо работать с внешними текстами).
Дело в том, что при enum нельзя присвоить const seller: Positions.Seller = 'Продавец';
Только из enum, будьте добры. С объектом - ok.
Такая штука.
Fundorin174 Автор
Так enum это и есть обёртка над объектами. Так же можно сказать что вместо async await можно использовать promise. Никто не спорит. Вкусовщина.