Так недавно я наткнулся на блок кода, который заставил меня задуматься на всю ночь. Код работал отлично, но что-то в нем меня действительно беспокоило, и я думал: «Можно ли его улучшить?»

Код более или менее похож на следующий, где isAllowed - это сетевой вызов внешнего API.

Так что меня беспокоило?

  • Слишком много ifs
  • Ошибки выкидываются одна за другой
  • Обрабатывает ошибку с помощью исключения

Как сделать жизнь лучше

На мой взгляд, почему мы не можем переписать код примерно так?

Он должен возвращать все ошибки проверки или возвращать ввод как действительный результат. И прежде чем некоторые из вас начнут кричать Promise.all, Нет! Простое использованиеPromise.all не приведет к тому, что я хочу здесь. Но об этом позже.

Итак, я отправился в путешествие по созданию модуля NPM, который может помочь лучше проверять объект JavaScript. Ну на самом деле не просто объект, а любые типы данных JavaScript. Давайте реорганизуем мой пример кода в верхней части этой статьи в более совершенный, и по ходу дела мы обнаружим, что фактически извлекаем логику проверки в модуль.

Кстати, я решил использовать TypeScript, чтобы помочь мне написать этот модуль, потому что сейчас он становится основным потоком, а типизированный язык помогает в построении хорошей системы.

Он должен возвращать все ошибки проверки или возвращать ввод как действительный результат.

Асинхронная функция и обещания

Прежде чем мы начнем, давайте вспомним асинхронные функции и промисы.

  • Асинхронные функции возвращают aPromise. Но обычная функция может возвращать Promise, не будучи помеченной как async.
  • Promise может быть разрешено / отклонено с любым значением или приводит к Error исключению
  • Асинхронные функции могут быть объединены в цепочку обещаний
  • Если вы ждете async функции с await, это означает, что вы находитесь внутри async функции
  • Error исключение и отклонение сразу прерывают цепочку обещаний
  • Вы можете запустить async функцию внутри async функции
  • В настоящее время появляется предупреждение, если вы не обрабатываете отклонение обещаний в Node и Chrome.

Я надеюсь, что точки имеют смысл. Если нет, не волнуйтесь, это станет яснее, когда мы начнем разгадывать, как этот модуль проверки должен работать.

Введите все

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

Тип данных человека довольно прост.

Далее, а не сразу бросать Error (a.k.a. exception), лучше правильно смоделировать ошибку и, таким образом, использовать их в чистых функциях. Таким образом, мы также полностью контролируем, что делать с ошибками.

Давайте сначала сформулируем очевидное: мы выбираем здесь имя Invalid, потому что уже существует класс с именемError, который представляет исключение.

Теперь некоторые из вас могут задаться вопросом, почему объект представляет ошибку вместо string? Дело в том, что во время выполнения набор текста в JavaScript очень ограничен. Если мы установим тип ошибки string, как мы будем различать ошибку и действительный string?

Но зачем нам создавать класс, а не просто использовать интерфейс? Потому что класс имеет лучшую встроенную проверку на Type Guard. Type Guard - это в основном способ проверить тип во время выполнения. Но об этом позже.

Всегда начинайте сначала с типа. TyDD, разработка на основе типов.

Перейдем к набору функции проверки.

Чтобы сделать функцию чистой, она должна иметь возвращаемое значение. Здесь мы используем InvalidOr<T> в качестве возвращаемого типа, который является типом объединения либо Invalid, либо T. T здесь - общий, это тип объекта, который мы проверяем. Это может быть string, array или даже объект. В нашем случае это Person. Итак, нашу функцию можно записать следующим образом

или, в качестве альтернативы, просто введите функцию вместо параметров

К сожалению, последнее может вызвать ошибку линтинга, если вы используете typescript-eslint настройку по умолчанию.

Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)

Чтобы исправить это, просто поместите возвращаемый тип в функцию.

Тип Охрана

А теперь вернемся к Type Guard. Как упоминалось ранее, это способ проверки типов во время выполнения. По сути, это функция.

isInvalid здесь проверяет, определено ли для входного объекта свойство errorMessage. Если это правда, то входной объект рассматривается как Invalid

Вот почему лучше иметь Invalid в качестве класса, потому что мы можем использовать instanceof в качестве защиты типа. Это строже, чем просто проверка собственности. См. Сравнение ниже

Но это еще не все, TypeScript достаточно умен, чтобы сузить тип, если есть if else оператор, использующий Type Guard.

Хотя технически result относится к типу InvalidOr<Person>, в первом блоке if он был сужен до Invalid, потому что мы используем Type Guard. Впоследствии он также сужается до Person в блоке else.

Попробуйте изменить выражение if с if(isInvalid(result)) на if(result instanceof Invalid), вы получите ошибку компиляции. Оба похожи, но различаются во время компиляции. В основном потому, что система типов TypeScript не так сильна, как другие языки ML, например. Haskell. В конце концов, он построен на динамическом языке. Вот почему в Type Guard у нас есть errorOr is Invalid, чтобы обозначить, что эта функция работает с Invalid подтипом Invalid | T

Синхронный запуск проверок

Как будто это еще не очевидно, нам нужно разделить логику проверки на функции, каждая из которых обрабатывает один вид проверки, то есть isAllowed, eighteenOrAbove и nonEmptyName. Для простоты давайте пока проигнорируем асинхронную функцию isAllowed. И давайте посмотрим, как мы можем закодировать реализацию выполнения синхронных проверок.

Это должно быть довольно просто. Помните, что мы хотим создать функцию, которая

  • принимает массив функций, которые принимают один и тот же ввод (также известные как Validate функции)
  • также взять входной объект для проверки
  • и он возвращает либо массив Invalid, либо исходный объект ввода.

Добро пожаловать в ValidateAll, который возвращает Validated<T>

Есть небольшой сюрприз, NonEmptyArray. По сути, это тип, который гарантирует, что массив не будет пустым во время компиляции. Это настраиваемый тип, который стал возможен благодаря TypeScript 3.0 остальные элементы в типах кортежей

Хорошо, теперь перейдем к реализации раннера.

На самом деле это довольно просто

  • mapвыполняют функции и выполняют их с входным объектом.
  • убедитесь, что мы перехватили исключение Error и поместили его в Invalid, потому что мы хотим сопоставить результат
  • проверьте результаты и сгруппируйте Invalid в массив с reduce
  • если найденных ошибок больше нуля, вернуть массив Invalid, иначе вернуть исходный объект. Обратите внимание, что это нужно делать через Type Guard, иначе компилятор TypeScript может пожаловаться.

И пример того, как использовать runValidations, будет примерно таким

Типы для асинхронной проверки

Асинхронная версия не сильно отличается. Начнем с типов

Как видите, AsyncValidate<T> похож на Validate<T>, а AsyncValidateAll похож на ValidateAll. Единственная разница в том, что результат заключен в Promise. Потому что помните, что функция async возвращает Promise.

Если мы хотим запустить все проверки, неважно, синхронные они или асинхронные, и оценить все результаты. Нам нужно будет превратить все синхронные проверки в асинхронные. Это довольно просто, просто добавьте async и оберните тип возврата в Promise

Затем мы помещаем все эти асинхронные функции в массив и передаем его в AsyncValidateAll, чтобы мы могли запускать проверки параллельно.

Асинхронный запуск проверок

Как выглядит асинхронный бегун?

Это немного сложнее, чем синхронная версия

  • Подобно runValidations, мы map над функциями и выполняем их с входным объектом
  • По сути, мы используемPromise.all для асинхронного получения значения из проверочных обещаний.
  • Однако перед передачей Promise.all нам нужно добавить каждый Promises, чтобы уловить и превратить перехваченные ошибки в Invalid
  • Остальные снова похожи на runValidations, сгруппируйте результаты проверки через reduce и используйте areInvalid Type Guard, чтобы определить, что возвращать.

Мы извлекаем функцию, которая отвечает за преобразование обнаруженных ошибок в Invalid, потому что она довольно особенная. Пойманный e на самом деле имеет any тип, в основном потому, что Promise.reject может отклонить что угодно, например строка, объект и т. д. Таким образом, нам нужно обращаться с этим осторожно, например, так

Пример использования runAsyncvalidations будет примерно таким:

Почему мы не можем просто использовать Promise.all?

Promise.all, в случае успеха вернет массив разрешенных Promise. А при ошибках вернет массив Errors

Это выглядит почти идеально, за исключением того, что при отклонении обещания Promise.all вернет только первое отклоненное обещание. Итак, если допустить ошибку на eighteenOrAbove, успешно на nonEmptyName и отклонить на IsAllowed, результатом будет возвращаемое значение отклоненного IsAllowed, и все. Это не то, что мы хотим.

Почему мы не можем просто связать проверки?

Проблема с вышеизложенным состоит в том, что любое исключение возникло из функции проверки, и любое отклонение обещания приводит к раннему завершению цепочки обещаний. Это означает, что мы не сопоставляем все ошибки проверки.

Эпилог

И вот, мы написали модуль проверки на TypeScript 😃. Этот модуль доступен на npmjs под названием falidator. Я только что выпустил его для бета-тестирования. Если вам интересно, пожалуйста, скачайте его и играйте. Любые отзывы приветствуются, и, как всегда, спасибо за чтение!