Валидация по TypeScript interface с использованием Joi

История о том, как потратить два дня на многократное переписывание одного и того же кода.
Содержание
Вступление
В рамках данной статьи опущу подробности про Hapi, Joi, роутинг и validate: { payload: ... }
, подразумевая, что вы уже понимаете о чём речь, как и терминологию, а-ля "интерфейсы", "типы" и тому подобное. Расскажу лишь о пошаговой, не самой удачной стратегии, своего обучения этим вещам.
Немного предыстории
Сейчас я единственный backend разработчик (именно, пишущий код) на проекте. Функциональность - не суть, но ключевая сущность - это довольно длинная анкета с личными данными. Скорость работы и качество кода завязано на моём малом опыте самостоятельной работы над проектами с нуля, ещё более малом опыте работы с JS (всего 4й месяц) и попутно, очень криво-косо, пишу на TypeScript (далее - TS). Сроки сжаты, булки сжаты, постоянно прилетают правки и получается сначала писать код бизнес-логики, а потом сверху интерфейсы. Тем не менее, технический долг способен догнать и настучать по шапке, что, примерно, с нами и случилось.
После 3х месяцев работы над проектом, договорился наконец с коллегами о переходе на единый словарь, чтобы везде свойства объекта назывались и писались одинаково. Под это дело, разумеется, взялся писать интерфейс и плотно застрял с ним на два рабочих дня.
Проблема
В качестве абстрактного примера выступит простая анкета пользователя.
ПервыйНулевой шаг хорошего разработчика:описать данныенаписать тесты;- Первый шаг:
написать тестыописать данные; - ну и так далее.
Допустим, на этот код уже написаны тесты, осталось описать данные:
Чтож, тут всё понятно и предельно просто. Весь этот код, как мы помним, на бэкэнде, а точнее, в api, то есть пользователь создаётся на основе данных, которые пришли по сети. Таким образом, нам нужно сделать валидацию входящих данных и поможет в этом Joi:
Решение "в лоб" готово. Очевидный минус такого подхода - валидатор полностью оторван от интерфейса. Если в процессе жизни приложения изменятся / добавятся поля или поменяется их тип, то данное изменение надо будет вручную отследить и указать в валидаторе. Думаю, таких ответственных разработчиков не будет до тех пор, пока что-то не упадёт. Кроме того, в нашем проекте, анкета состоит из 50+ полей на трёх уровнях вложенности и разбираться в этом крайне сложно, даже зная всё наизусть.
Просто указать const joiUserValidator: IUser
мы не можем, потому что Joi
использует свои типы данных, что порождает при компиляции ошибки вида Type 'NumberSchema' is not assignable to type 'number'
. Но ведь должен быть способ выполнить валидацию по интерфейсу?
Возможно, я неправильно гуглил, или плохо изучал ответы, но все решения сводились к либе extractTypes
и каким-то лютым велосипедам, типа такого:
Решение
Использовать сторонние библиотеки
Почему бы нет. Когда я вопрошал к людям со своей задачей, то получил в одном из ответов, а позже, и тут, в комментариях (спасибо @keenondrums ), ссылки на данные библиотеки: https://github.com/typestack/class-validator https://github.com/typestack/class-transformer
Однако, был интерес разобраться самому, понять лучше работу TS, да и ничего не поджимало решить задачу сиюминутно.
Получить все свойства
Поскольку со статикой ранее дел я не имел, вышеуказанный код открыл Америку в плане применения тернарных операторов в типах. К счастью, применить его в проекте не удалось. Зато нашёл другой интересный велосипед:
TypeScript
при довольно хитрых и загадочных условиях позволяет получить, например, ключи из интерфейса, словно это нормальный JS-объект, правда, только в конструкции type
и через key in keyof T
и только через дженерики. В результате работы типа UserKeys
, у всех объектов, реализующих интерфейсы, должен быть одинаковый набор свойств, но при этом типы значений могут быть произвольные. Это включает подсказки в IDE, но всё ещё не даёт однозначно обозначить типы значений.
Здесь есть ещё один интересный кейс, который не смог использовать. Возможно, вы подскажете зачем это нужно (хотя я частично догадываюсь, не хватает прикладного примера):
Использовать вариативные типы
Можно явно задать типы, а используя "ИЛИ" и извлечение свойств, получить локально работоспособный код:
Проблема этого кода проявляется когда мы хотим забрать валидный объект, например, из базы, то есть TS заранее не знает какого типа данные будут - простые или Joi. Это может вызвать ошибку при попытке выполнить математические операции с полем, которое ожидается как number
:
Данная ошибка приходит из Joi.NumberSchema
потому что возраст может быть не только number
. За что боролись на то и напоролись.
Соединить два решения в одно?
Где-то к этому моменту рабочий день подходил к логическому завершению. Я перевёл дух, выпил кофе и стёр эту порнографию к чертям. Надо меньше читать эти ваши интернеты! Настало время взять дробовик и пораскинуть мозгами:
0. Объект должен формироваться с явными типами значений;
- Можно использовать дженерики, чтобы прокидывать типы в один интерфейс;
- Дженерики поддерживают типы по умолчанию;
- Конструкция
type
явно способна на что-то ещё.
Пишем интерфейс-дженерик с типами по умолчанию:
Для Joi можно было бы создать второй интерфейс, наследовав основной таким образом:
Недостаточно хорошо, ведь следующий разработчик может с лёгким сердцем расширить IUserJoi
или что похуже. Более ограниченный вариант получить похожее поведение:
Пробуем:
Компилится, на месте использования выглядит аккуратно и при отсутствии особых условий всегда устанавливает типы по умолчанию! Красота...
...на что я потратил два рабочих дня
Резюмирование
Какие выводы из всего этого можно сделать:
- Очевидно, я не научился находить ответы на вопросы. Наверняка при удачном запросе это решение (а то и ещё лучше) находится в первой 5ке ссылок поисковика;
- Переключиться на статическое мышление с динамического не так просто, гораздо чаще я просто забиваю на такое копошение;
- Дженерики - крутая штука. На хабре и стековерфлоу полно
велосипедовнеочевидных решений для построения сильной типизации...вне рантайма.
Что мы выиграли:
- При изменении интерфейса отваливается весь код, включая валидатор;
- В редакторе появились подсказки по именам свойств и типам значений объекта для написания валидатора;
- Отсутствие непонятных сторонних библиотек для тех же целей;
- Правила Joi будут применяться только там, где это нужно, в остальных случаях - дефолтные типы;
- Если кто-то захочет поменять тип значения какого-то свойства, то при правильной организации кода, он попадёт в то место, где вместе собраны все типы, связанные с этим свойством;
- Научились красиво и просто скрывать дженерики за абстракцией
type
, визуально разгружая код от монструзоных конструкций.
Мораль: Опыт бесценен, для остального есть карта "Мир".
Посмотреть, пощупать, запустить итоговый результат можно:
https://repl.it/@Melodyn/Joi-by-interface
PS
Посмотреть на другие вариации решений по данной задаче можно в комментариях на Хабре
Sergei Melodyn
6 лет назад