Валидация по 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
7 лет назад
4






