«Parse, don't validate» в языке, который сопротивляется: беда структурной типизации TypeScript
Идея старая и любимая: превратить валидацию в тип, который компилятор запомнит. Загвоздка, доказывает автор блога, в том, что TypeScript считает одинаковые по форме типы тождественными, поэтому Email — это просто string, и гарантия испаряется.
Эссе разбирает, как заставить принцип «parse, don't validate» работать в TypeScript, где из-за структурной типизации проверенный Email неотличим от любой строки. Лекарство — branded-тип: фантомный символ, из-за которого Email несовместим со string на этапе компиляции и исчезает в рантайме.
Старожилы треда разошлись, настоящая ли это идея или подмена: один считает, что брендирование лишь навешивает на строку ярлык «проверено», другой возражает, что любая гарантия типа — это в конечном счёте валидация, которой компилятор согласился доверять.
Различие, на котором держится всё эссе
Пост Кристиана Экрема воскрешает принцип Алексис Кинг и спрашивает, переживает ли он столкновение с TypeScript. Формулировка разницы у автора точная: «валидатор говорит: с этой штукой всё в порядке, продолжай. Парсер говорит: дай мне комок данных, и я либо верну более точный тип, либо объясню, почему не могу». Валидатор выбрасывает свою работу: как только email: string прошёл проверку, система типов об этом забывает, поэтому та же защитная проверка повторяется ниже по коду — Кинг называла это shotgun parsing.
Препятствие — структурная типизация TypeScript: два типа с одинаковой формой суть один тип, поэтому Email не может быть по-настоящему отличен от string. Язык, пишет Экрем, «не будет настаивать и даже мягко не подтолкнёт» вас к парсингу. Его обходной приём — branded-тип: фантомный уникальный символ, который делает Email несовместимым с обычным string на этапе компиляции и стирается в ничто в рантайме, а единственная граница, где такой тип разрешено создавать, — это парсер. Он противопоставляет этому Elm, Haskell и F#, которые тянут вас к парсингу через непрозрачные типы и умные конструкторы, и приходит к выводу про образ мышления, а не библиотеку: «пусть доказательство несёт система типов, а не ваша память».
Branded-тип — это правда «парсинг» или валидация со значком на груди?
Самый острый обмен репликами поставил под вопрос, является ли брендирование тем самым приёмом вообще. Один инженер доказывал, что оно недотягивает до исходной идеи:
«Это просто валидация, которая использует систему типов, чтобы обозначить, что валидация уже произошла… вместо того чтобы структура типа несла эту информацию сама по себе (например, struct Email { name: String, host: String }).»
jerf, в затянувшейся ветке, стёр это различие: «Это одно и то же… Любое нетривиальное использование системы типов подразумевает гарантии, которые сама система типов выразить не в состоянии». Гарантия «первый-и-последний» у списка NonEmpty — тоже валидация, которую кто-то однажды прогнал, а затем закодировал, чтобы вызывающий код её не повторял; брендирование — более слабая точка на том же континууме, а не другой вид.
Вопрос, в который упирается любая TS-команда: не взрывается ли это до 2^N типов?
Практическое возражение звучало снова и снова: если парсить вместо валидации, нужны ли UserWithBirthday, VerifiedUser, UserWithBirthdayAndEmail — по типу на каждую комбинацию присутствующих полей? Комментатор, работающий в DDD, опасался, что тут «что-то не так». Самый сильный ответ провёл границу. Один инженер показал, что взрыв мнимый: независимое необязательное поле остаётся единственным T | null; к отдельным типам тянешься только тогда, когда поля скоррелированы как состояние, «и тогда это размеченное объединение по полю status, а это N состояний, а не 2^N». Другой назвал ответ из теории — refinement types, привязывающие предикаты, проверяемые на этапе компиляции, — и дал прагматичное правило: «String -> User того стоит, User -> UserWithBirthday — нет».
К чему в итоге приходит зал: Zod на границе
При всей гимнастике с системой типов инструмент, на котором сошлись, оказался проще. Комментатор, глубоко сидящий в функциональном TypeScript, и ещё несколько человек сошлись на том, чтобы валидировать схемной библиотекой на границе программы, а внутри держать обычные типы: «Zod эргономичнее, у него проще API, и для большинства он идеален». Один читатель прямо взвесил цену и пользу: branded-типы и newtypes — это круто, но дорого в обслуживании, и «если вы не строите библиотеку, от которой зависят миллионы, оно, скорее всего, того не стоит». Принцип эссе язык переживает; а вот акробатика с фантомным символом, которая его доказывает, дальше границы по большей части не уходит.
Получайте такие разборы каждый день
Главные истории Hacker News — на email, в Telegram или RSS.
Он спросил, перезагружали ли вы Mac на этой неделе. Начались войны за аптайм
Пост был маленьким субботним ритуалом: перезагрузить Mac, снести окно «вы уверены?» и почувствовать себя чуть бунтарём. Тред превратил это в спор о том, что вообще означает перезагрузка.