Введение
Решим реальную практическую задачу, с которой мне пришлось столкнуться на моем проекте React/TypeScript.
Задача
У нас есть массив конкретных строковых значений, таких как "first", "second", "third", "fourth" и "fifth". Необходимо отобразить их на странице, т.е как-то использовать, а также убедиться, чтобы эти данные были строго типизированы и TypeScript нам выдавал всплывающие подсказки при их использовании в коде.
Решение
Сначала поместим данные в неизменяемый (readonly) массив:
const numberNames = ["first", "second", "third", "fourth", "fifth"] as const
Мы объявили константу numberNames и присвоили ей массив строковых литералов;
Конструкция as const используется чтобы сделать из данного массива неизменяемый кортеж (tuple), где конкретные строковые литералы сохраняются как литеральные типы;
Сохранение строковых литералов в качестве литеральных типов означает, что компилятор TypeScript рассматривает каждый элемент в массиве как отдельный тип, соответствующий его точному строковому значению. Например, тип первого элемента 'first' не является просто общим строковым типом (string), а конкретно типом 'first';
Убедитесь в этом сами, создав два массива - один с as const, а другой без. А затем наведите на переменную и посмотрите разницу.
Теперь создадим тип на основе этого массива:
type NumberName = typeof numberNames[number]
Здесь мы создали тип NumberName, значением которого является объединение (union) типов строковых литералов из массива;
Индексатор числа [number] извлекает объединенный тип отдельных элементов массива. В результате typeof numberNames[number] представляет собой объединенный тип элементов массива:
"first" | "second" | "third" | "fourth" | "fifth"
Почему [number], а не, например [string]? Все просто - как мы знаем, к элементам массива можно получить доступ через числовой индекс: например,
numberNames[0] => "first"
;Даже если бы у нас был массив со значениями других типов, например [1, 'blabla', 555], то в любом случае мы бы использовали [number] для того, чтобы создать тип объединения на его основе
1 | 'blabla' | 555
;Кстати, данная возможность извлечения типа из кортежа при помощи индексатора [number], как у нас в примере, появилась в версии TypeScript 4.1.
Заключение
Для чего это все нужно - чтобы строго типизировать данный список строк и когда мы будем с ним работать, получать такого рода подсказки:
Я выбрал выбор типов (Type inference) и as const в качестве решения, потому что хотел реализовать его максимально гибким и с минимальным количеством кода.
P.S. Это мои первые шаги в области написания веб-статей, и это соответственно моя первая статья. Поэтому я хотел начать с чего-то небольшого, чтобы поделиться полезной информацией со всеми вами. Буду рад вашим комментариям. Спасибо!
Комментарии (23)
UP_IM
24.07.2023 17:51А почему не использовать enum для этих целей?
mavludin Автор
24.07.2023 17:51Можно и enum, но я конкретно рассматривал решение с наименьшим количеством кода.
Ну и если массив в будущем будет дополняться, нам придется в enum еще лезть и дополнять все вручную.UP_IM
24.07.2023 17:51Так же как и в вашем случае, тоже придется дополнить варианты возможных значений))
mavludin Автор
24.07.2023 17:51С enum вот так например:
enum NumberNamesEnum { First = "first", Second = "second", Third = "third", Fourth = "fourth", Fifth = "fifth", } const numberNames: NumberNamesEnum[] = [ NumberNamesEnum.First, NumberNamesEnum.Second, NumberNamesEnum.Third, NumberNamesEnum.Fourth, NumberNamesEnum.Fifth, ];
И это все надо держать в актуальном состоянии, и потом чтоб использовать - импортировать везде.
Но с другой стороны такое выглядит лучше задокументированным и в сравнении с моим решением не нужно голые строки везде использовать.ubx7b8
24.07.2023 17:51Конкретно в этом случае можно сделать:
const numberNames: NumberNamesEnum[] = Object.values(NumberNamesEnum);
но если сделать "const enum", то так уже не получится.
Kirill-112
24.07.2023 17:51+4Пожалуйста, не надо использовать енумки в качестве объектов. Они не предназначены для этого.
serginho
24.07.2023 17:51+2Как минимум один плюс: для передачи литерала в качестве параметра вам не нужно дополнительно импортировать enum и искать, откуда его импортировать.
azizoid
24.07.2023 17:51+2enum очень ужасен, аж один из разработчиков тайпскрипта говорил что есои бы пришлось изобретать enum сейчас то они не стали бы это делать.
Вместо enum многие (и я) пользуются объектами с последующим `keyof typeof numberNames`
arusakov
24.07.2023 17:51А может еще добавите в статью, в какой версии TypeScript появилась возможность вот так использовать "индексатор числа [number]". Это может быть полезно для тех, кто внимательно следил за TypeScript раньше, а сейчас немного ослабил хватку :)
SWATOPLUS
24.07.2023 17:51+2Спасибо. Рад в кое-то веке видеть небольшую, но информативную техническую статью на хабре.
Так же хочу показать ещё один интересный пример:
const args = [8, 5] as const; const angle = Math.atan2(...args);
Так как к массиву применен
as const
его длинна известна на стадии компиляции. ФункцияMath.atan2
принимает только два аргумента, и, так как компилятор знает о длине массива, позволяет использовать spread-оператор. Если убратьas const
, то компилятор не будет знать о длине массива, и не разрешит такой вызов.
FluffyArt
24.07.2023 17:51Удивительно, что на две бытовых строчки кода такая объемная статься вышла. И название такое, что думал что-то необычное увидеть
Но совсем уж начинающим может пригодиться, жаль, что такую статью через Гугл не найти, слишком сложное для этого название. На вашем месте, я бы упростил
Кому интересно, какие плюсы у этого подхода:
Если смотреть фронтенд - не надо импортить везде как enum, и что ещё хуже добавлять в контекст компонента для того, чтобы он попал в рендер (привет vue2)
Удобно описать валидатор для конечной точки в цепочке компонентов (функций). В цепочке проверяем тип (и удобно указываем значения), в конце проверяем, что никто не навлевал на требуемый тип, ибо ts не гарантия :)
mavludin Автор
24.07.2023 17:51Я гуглил и смотрел какой перевод у понятия type inference. Везде было "Вывод типов". Решил так и использовать, хотя может можно было что-то типа "Вычисление типа по массиву строковых значений".
Статья вышла в таком объёме, в каком я сам решил для аудитории тех, кто с таким подходом не знаком, с одной стороны стараясь не поливать воду, а с другой стараясь ничего не пропустить и простым языком дать объяснение.
dopusteam
Не проще ли было сделать?
dlc
Не проще по двум причинам:
1) В таком случае у вас уже есть обычный массив с данными и его можно использовать, как массив во вс\м коде.
2) Мы просто выводим тип из этого массива и этот тип всегда будет синхронизирован с реальными данными в массиве.
По факту эдакая замена
enum
, но с расширенными структурами данных.mavludin Автор
Спасибо за разъяснение - так и есть, можно сделать и иными способами, но я конкретно рассматривал решение с наименьшим количеством кода.
Ну и если массив в будущем будет дополняться, неудобно же все вручную прописывать при помощи
enum
илиtype MyType ="first" | "second" | ...
dopusteam
Я б это в статью добавил, хорошие аргументы
mavludin Автор
Просто если расписывать разницу с enum, статья растянется и это изначально не являлось целью этой статьи.
Kirill-112
Приходится повторяться потом:
const arr: MyType[] = ["first", "second", ...];