TDD — Type Driven Development
Движущая сила: типы как средство верификации инвариантов (компилятор как автоматический пруфер).
Уровень применения: код и архитектура (зависит от языка и выразительности системы типов).
Статус: нишевый (языки с продвинутыми типами: Haskell, Idris, F#, OCaml, Scala, TypeScript в strict-режиме).
Не путать с: TDD — Test Driven Development (та же аббревиатура, но движущая сила — тесты, а не типы). Это критическая дизамбигуация: см. TDD — Test Driven Development. Там «зелёный» означает, что тесты проходят; здесь — что код компилируется. Два разных механизма верификации.
Общее
Type Driven Development (Type-DD) — методология разработки, в которой типы выступают первичным инструментом проектирования и верификации: разработчик сначала уточняет типы так, чтобы они выражали инварианты предметной области, и лишь затем пишет реализацию. Цикл построен вокруг компилятора: тип, который «не сходится», — это аналог красного теста; код, который компилируется, — аналог зелёного. Ключевая идея: если типы спроектированы так, что некорректное состояние нельзя даже выразить, то значительный класс дефектов отлавливается на этапе компиляции — автоматически и бесплатно, без написания отдельных проверок.
Термин в современном значении введён Эдвином Брэди (Edwin Brady) — автором языка Idris — в книге Type-Driven Development with Idris (Manning, 2017). Брэди оформил подход как дисциплинированный цикл «тип → реализация → уточнение», опирающийся на выразительные системы типов. Исторические корни метода глубже: зависные типы (dependent types) и сообщество теоремо-доказательства (theorem proving) — Coq, Agda, Lean — где типы буквально являются утверждениями, а программа — конструктивным доказательством. Type-DD — практическая, инженерная проекция этой теоретической традиции, перенесённая в «обычные» функциональные языки.
Фундамент, на который опирается вся методология, — соответствие Карри — Ховарда (Curry – Howard correspondence): наблюдение, что типы изоморфны логическим утверждениям, а программа, имеющая данный тип, — доказательству этого утверждения. Функция f: A → B читается как «из доказательства A следует доказательство B»; тип, у которого есть хотя бы одно значение (обитаемый тип), — как истинное утверждение; тип, у которого нет значений, — как ложное. В языках с зависными типами (Idris, Agda) это соответствие работает в полную силу: можно сформулировать тип «список отсортирован» или «длина списка равна N» и потребовать от функции доказать его. В практическом Type-TDD соответствие работает в ослабленном, но полезном виде: компилятор выступает автоматическим прувером для ограниченного класса утверждений — что поле инициализировано, какие переходы состояний возможны, исключён ли null. Главное следствие для инженера: «компилируется» перестаёт быть формальностью и становится осмысленным подтверждением корректности — разумеется, в рамках того, что удалось выразить в типах.
Дизамбигуация: Type-TDD и Test-TDD — это разные вещи
Это самая частая путаница в обсуждениях «TDD». Оба подхода носят аббревиатуру TDD, оба построены вокруг цикла с «красным» и «зелёным» состояниями, но механизмы верификации у них принципиально разные:
| Ось | Type Driven Development | Test Driven Development |
|---|---|---|
| Что выступает спецификацией | Типы (сигнатуры, инварианты в системе типов) | Тесты (исполняемые проверки поведения) |
| «Красный» (незавершённость) | Код не компилируется (тип не сходится) | Тест падает |
| «Зелёный» (готовность) | Код компилируется | Тесты проходят |
| Что верифицируется | Структурные инварианты, невозможность некорректных состояний | Поведение: вход → ожидаемый выход |
| Цена ошибки | Падает на этапе компиляции (до запуска) | Падает при запуске тестов (в CI/локально) |
| Что НЕ ловится | Бизнес-логика, конкретные значения, эффекты ввода-вывода | Структурные ошибки, если о них не догадались написать тест |
Подходы не конкурируют, а дополняют друг друга: типы страхуют структуру и инварианты (то, что выражается в сигнатурах), тесты — поведение и конкретные значения (то, что типами выразить нельзя). В зрелой функциональной кодовой базе они живут рядом. Подробно о тестовом TDD — в отдельной статье TDD — Test Driven Development.
Ключевые принципы
Type-TDD — это не «писать аннотации типов» (это средство), а набор принципов, которые в совокупности меняют то, как код проектируется. Ниже — формулировка каждого с пояснением, какую конкретную проблему он решает.
Сделай некорректные состояния непредставимыми (make illegal states unrepresentable). Фраза, популяризованная Яроном Мински (Yaron Minsky, Jane Street) в докладе Effective ML, — самый известный лозунг подхода. Проблема: если доменная модель допускает невалидные комбинации значений (например, «временной интервал с отрицательной длительностью» или «заказ без товаров»), то проверки этих состояний размазываются по рантайму — где-то проверили, где-то забыли. Type-TDD переносит проверку в типы: если интервал представлен парой «начало» и «длительность-положительное», то отрицательный интервал невозможно сконструировать, и проверять нечего. Связанная максима — parse, don’t validate (Алексис Кинг): данные проверяются один раз на границе системы, где «сырой» вход преобразуется в типизированное представление, а дальше всё, что имеет правильный тип, гарантированно валидно.
Типы как спецификация. Сигнатура функции — это не комментарий, а исполняемое машиной (компилятором) утверждение о том, что функция принимает и возвращает. Проблема: в слабо типизированных подписях спецификация живёт в голове разработчика и в комментариях, которые неизбежно устаревают. Выразительная сигнатура документирует контракт так, что рассинхронизация физически невозможна — компилятор не даст ей возникнуть.
Пошаговое уточнение типов (type refinement). Разработка ведётся как последовательное уточнение: от грубого типа («строка») к более точному («непустая строка», «email», «идентификатор пользователя»). Каждое уточнение сужает множество допустимых значений и автоматически переносит проверки вверх, к месту конструирования. Проблема, которую решает: слишком общий тип разрешает слишком много, и дефекты всплывают далеко от причины — там, где значение впервые приняло «плохую» форму, но это никто не заметил.
Компиляция как доказательство (compile as theorem proving). В пределе — в языках с зависными типами — тип буквально является теоремой, а принявшая его программа — доказательством (соответствие Карри — Ховарда). В практическом Type-TDD этот принцип работает в ослабленном виде: компилятор выступает автоматическим прувером для ограниченного, но ценного класса утверждений (инициализировано ли поле, какие переходы состояний возможны, исключён ли null). Проблема: ручные проверки и ревью уязвимы для человеческой невнимательности; компилятор — нет.
Design by contract через типы. Классическая идея контрактов (предусловия, постусловия, инварианты), оформленная Берtrandом Мейером для Eiffel, в Type-TDD реализуется не рантайм-проверками, а типами: предусловие превращается в ограничение на входной тип, постусловие — в ограничение на возвращаемый, инвариант — в тип поля. Проблема: рантайм-контракты проверяются только когда код выполняется по конкретному пути; типизированные контракты проверяются всегда, для всего кода.
Тип управляет дизайном, а не украшает его. Как и в тестовом TDD, где тест пишется первым и вынуждает лучший интерфейс, в Type-TDD тип пишется первым и вынуждает лучшую модель данных. Разработчик волей-неволей продумывает допустимые состояния до того, как начинает их обрабатывать. Это и есть смысл фразы «type-driven»: тип ведёт за собой реализацию, а не подгоняется под неё постфактум.
Принципы — система, а не меню: «непредставимые состояния» без пошагового уточнения остаются лозунгом; «компиляция как доказательство» без design by contract вырождается в формализм ради формализма. Частичное применение даёт частичный эффект; связное применение — заявленный сдвиг багов на этап компиляции.
Как это работает
Рассмотрим цикл на минимальном примере. Задача — смоделировать аутентификацию с двумя состояниями: «не залогинен» и «залогинен». Цель — показать механику, а не научить языку; код намеренно иллюстративен (псевдокод в стиле статически типизированного FP-языка).
Классический, «нетипизированный» подход хранит состояние в одном типе и проверяет его в рантайме:
session.state = "logged_out" // строка — что угодно допустимо
session.state = "logged_in"
def fetch_profile(session):
if session.state != "logged_in":
raise NotAuthenticatedError // проверка на каждом вызове
...
Проблема: ничего не мешает вызвать fetch_profile у незалогиненной сессии — ошибка всплывёт только в рантайме, и только если этот путь кода выполнился. Проверку дублируют везде, где-то забывают.
Шаг 1. Напиши тип (Red). Смоделируем состояния как два разных типа, так что незалогиненную сессию нельзя даже передать туда, где ждут залогиненную:
data LoggedOut = LoggedOut
data LoggedIn = LoggedIn(token: Token)
def login(creds: Credentials): LoggedIn
def logout(s: LoggedIn): LoggedOut
def fetch_profile(s: LoggedIn): Profile
Попытка вызвать fetch_profile(LoggedOut) не компилируется: тип аргумента не сходится. Это и есть «красный» этап — но теперь он ловит структурную невозможность ещё до запуска.
Шаг 2. Уточни реализацию (Green). Пишем реализацию под зафиксированные типы. Переход «залогиниться» возвращает LoggedIn с токеном; «разлогиниться» принимает только LoggedIn и возвращает LoggedOut:
def login(creds):
if valid(creds):
return LoggedIn(issue_token(creds))
else:
raise InvalidCredentials # отказ — это рантайм-событие,
# его типом не выразить
def fetch_profile(s: LoggedIn):
return api.get_profile(s.token)
После того как код принят компилятором, состояние «незалогиненный пользователь вызывает fetch_profile» стало непредставимым — такой вызов не проходит проверку типов. Рантайм-проверка if state != logged_in больше не нужна и удалена.
Шаг 3. Рефактори. Уточняем модель дальше. Например, токен с истёкшим сроком — отдельный тип (ExpiredToken), и его нельзя передать в fetch_profile; обновление токена становится функцией refresh: ExpiredToken → LoggedIn. Каждый такой шаг сужает множество рантайм-проверек и расширяет множество гарантий, даваемых компилятором.
Цикл повторяется: уточни тип → убедись, что старый код перестал компилироваться (это сигнал, что нашли упущенный инвариант) → поправь реализацию → проверь, что компилируется. На каждом витке часть проверок переезжает из рантайма в систему типов.
Важная оговорка, не очевидная снаружи: Type-TDD не устраняет рантайм-проверки полностью. Любое решение, принимаемое по данным, которые приходят извне (ввод пользователя, ответ API, чтение файла), должно где-то проверяться — но проверяется один раз, на границе, где «сырые» данные преобразуются в тип. Дальше работает уже тип. Кроме того, типы не выражают конкретные бизнес-значения: «сумма перевода не больше лимита» или «email соответствует реальному домену» — это по-прежнему территория тестов. Поэтому зрелая Type-TDD-кодовая база всегда сопровождается обычными тестами для бизнес-логики.
Пример 2: parse, don’t validate
Первый пример показал, как типы делают переход состояний безопасным. Второй — другая ипостась подхода: как типы убирают рантайм-проверки из потока данных, перенося их на границу системы. Это и есть максима parse, don’t validate (Алексис Кинг): данные проверяются один раз, при входе, и дальше везде, где они проходят как значение правильного типа, гарантированно валидны.
Задача: регистрация пользователя. На входе — «сырой» JSON произвольной формы; в ядре системы хочется работать только с корректными значениями. Типичный, нетипизированный подход тянет проверки через весь конвейер:
def register(body):
name = body.get("name") # может быть None
email = body.get("email") # может быть None, не email
age = body.get("age") # может быть None, отрицательным, строкой
if name is None or email is None: # проверка №1
raise BadRequest
if not is_email(email): # проверка №2
raise BadRequest
if age is None or age < 0: # проверка №3
raise BadRequest
...
save_user(name, email, age) # а внутри — ещё проверки «на всякий случай»»
Проблема та же, что и в первом примере: проверки размазаны, дублируются, забываются. Любой downstream-код, получающий email, не имеет гарантии, что это действительно email, — поэтому перестраховывается снова.
Уточняем типы. Введём «умные» типы, которые можно сконструировать только из валидных данных:
data NonEmptyString = NonEmptyString(str) # только если str != ""
data Email = Email(str) # только если str проходит regex
data Age = Age(int) # только если 0 <= int <= 150
# Единственный конструктор, способный «упасть» — он и есть граница:
def parse_registration(body): Result[Registration, Error]
# внутри — все проверки ровно один раз;
# на выходе — либо Registration с типизированными полями, либо Error
Теперь ядро системы работает с Registration — записью, чьи поля уже имеют типы NonEmptyString, Email, Age:
def save_user(r: Registration):
db.insert(r.name.value, r.email.value, r.age.value)
# никаких if-проверок: тип гарантировал их при конструировании
Что изменилось:
- Проверки сгруппированы в одном месте (
parse_registration) — границе системы. - Всюду ниже по потоку сигнатуры функций принимают только типизированные значения, и компилятор не даст передать туда «сырую» строку. Невозможно случайно вызвать
save_userс нетипизированным вводом — это просто не скомпилируется. - Удаление проверок безопасно: если в будущем поле
emailпоменяет представление, компилятор укажет каждое место, которое нужно поправить.
Это и есть практическое воплощение «make illegal states unrepresentable»: невалидная регистрация не просто отбрасывается — её невозможно сконструировать как значение типа Registration. Цикл Type-TDD здесь выглядит как постепенное сужение: от str → к NonEmptyString → к Email, каждый шаг отъедает часть рантайм-проверек и превращает их в гарантии компилятора.
Влияние на команду и процесс
Type-TDD часто обсуждают как свойство языка, но устойчивый эффект возникает только тогда, когда подход встроен в командные практики. Ниже — что именно меняется, когда проектирование «от типов» становится нормой. Этот блок адресован прежде всего руководителям.
Жёсткая зависимость от стека. В отличие от тестового TDD, который применим на любом языке, Type-TDD требует языка с достаточно выразительной системой типов. Полная версия (зависные типы, теоремы как типы) доступна в Idris, Agda, Lean — нишевых языках исследований. Практическая версия — алгебраические типы данных, pattern matching с проверкой полноты, обобщённые типы, Option/Result вместо null — доступна в Haskell, F#, OCaml, Scala, Rust, а в ослабленном виде и в TypeScript (strict-режим) или Kotlin. На динамических языках (Python, Ruby, JavaScript без TS) подход неприменим в принципе. Это значит: выбор Type-TDD — это часто выбор языка и, следовательно, целой экосистемы найма, библиотек и инфраструктуры.
Порог входа выше среднего. Продвинутые системы типов — алгебраические типы, типы высшего рода, фантомные типы, GADT — требуют обучения. Команда, пришедшая из динамических или «предприятийских» ООП-языков, переживает заметный период адаптации: ревью замедляются, код первым делом «не читается», потому что типы выглядят сложно. Проблема управленческая: закладывать время на обучение нужно явно, а не «в порядке естественного хода».
Сдвиг багов на этап компиляции (shift-left). Главная ценность подхода для процесса — дефекты класса «некорректное состояние» ловятся не тестами и не в продакшене, а компилятором, ещё до запуска. Это радикально сужает множество регрессий: невозможно «случайно» передать незалогиненную сессию туда, где ждут залогиненную, потому что такой код просто не соберётся. Для safety-critical доменов это не удобство, а требование.
Влияние на найм. Type-TDD сужает воронку: инженеров со свободным владением продвинутыми типами заметно меньше, чем «обычных» разработчиков. С одной стороны, это фильтр качества — носители таких навыков, как правило, сильны системно. С другой — это объективное ограничение скорости роста команды, особенно вне хабов концентрации FP-сообщества. Закладывать это в планы расширения нужно реалистично.
Ревью смещается на типы. На code review акцент переходит с построчной проверки логики на ревью модели данных: правильно ли выбраны типы, какие состояния допускаются, нет ли «слишком широких» сигнатур. Это быстрее и одновременно глубже, чем поиск рантайм-ошибок глазами, но требует от ревьюеров навыка читать типы как спецификацию.
Самодокументируемость и онбординг. Выразительные сигнатуры играют роль живой документации: новый разработчик видит допустимые переходы и инварианты прямо в типах, без чтения комментариев (которые устарели бы). Это снижает порог входа в кодовую базу — но только после того, как преодолён порог входа в сам язык.
Синергия с тестовым TDD. На практике Type-TDD и Test-TDD работают вместе: типы страхуют структуру и инварианты, тесты — конкретное поведение и значения. Команда, владеющая обоими, получает двухуровневую верификацию, где каждый слой ловит свой класс дефектов. В AIDD типы дополнительно служат контрактом для AI-генерации: чем точнее тип, тем меньше свободы у модели ошибиться.
Динамика скорости: медленнее старт — стабильнее потом. Как и у тестового TDD, у Type-TDD есть «долгий порог»: первые недели-месяцы скорость разработки ниже, потому что время уходит на проектирование типов, а не на «быстрые фичи». Компенсация приходит позже — в виде радикально более дешёвого рефакторинга и почти полного исчезновения регрессий по состояниям. Для руководителя это означает: метрики velocity в первые спринты проседают, а стабильность и предсказуемость в долгую — растут. Оценивать подход по скорости первых итераций — та же разрушительная ошибка, что и с Test-TDD: эффект проявляется на горизонте месяцев. Полезный ориентир — отслеживать не «строки в день», а «плотность runtime-дефектов»: именно она должна снижаться по мере того, как модель обрастает точными типами.
Преимущества
Дефекты ловятся компилятором. Самая прямая выгода: целый класс ошибок (некорректные состояния, неполный pattern matching, null-разыменование) обнаруживается при сборке, а не в рантайме. Регрессия этого класса невозможна — сломанный код просто не соберётся.
Самодокументирующиеся инварианты. То, что в слабо типизированном коде живёт в комментариях и в голове разработчика, здесь записано в типах и проверяется каждый раз. Документация такого рода физически не может устареть: если тип и реализация расходятся, сборка падает.
Безопасный рефакторинг. Компилятор, проверяющий инварианты, работает как «зелёный набор тестов» для структурных изменений. Переименования, смена представления данных, перераспределение логики между модулями страхуются типами так же, как в Test-TDD они страхуются тестами — только для другого класса изменений.
Меньше рантайм-проверек. Проверки валидности переезжают на границу системы (parse, don’t validate). Внутри доменного ядра код становится короче и прямолинейнее: вместо if x is not None and x.field > 0 — прямая работа со значением нужного типа.
Типы как контракт для коллаборации. Сигнатура — это однозначный интерфейс между модулями и между разработчиками. Спор «что эта функция может вернуть» разрешается компилятором, а не обсуждением. Это снижает трения в команде и ускоряет параллельную разработку.
Дисциплинирующий эффект на модель. Необходимость выразить инвариант в типе заставляет задуматься о модели данных раньше, чем она «застынет» в коде. Как и в DDD, где домен ведёт архитектуру, в Type-TDD типы ведут модель.
Недостатки и риски
Доступен не везде. Главный практический минус: подход требует языка с выразительной системой типов, а таких языков в индустрии меньшинство. На динамических языках (Python, Ruby) или со «слабой» статикой (ранний Java/C#) Type-TDD неприменим в полном виде — только как частичные практики (аннотации, опциональные типы).
Крутая кривая обучения. Продвинутые типы — концептуально сложная тема. Команда без FP-фона переживает долгий период адаптации, в течение которого скорость разработки падает, а сложность кода на ревью — растёт. Без выделенного времени на обучение внедрение часто даёт обратный эффект.
Риск over-typing (типы ради типов). Соблазн выразить в типах всё, включая то, что естественнее живёт в бизнес-логике, — главная болезнь неопытных адептов. Результат: код, в котором типы сложнее самой логики, ревью превращаются в криптографию, а любое изменение требования требует переписывания половины системы типов. Зрелость Type-TDD — в умении отличить инвариант (выражать в типе) от проверки значения (оставлять в коде и тестах).
Не заменяет тесты. Типы верифицируют структуру и инварианты, но не конкретное поведение. «Функция возвращает число» — тип; «функция возвращает правильное число» — тест. Бизнес-логика, граничные значения, эффекты ввода-вывода — по-прежнему территория тестов. Команда, решившая, что «раз код компилируется, тесты не нужны», получает не проверенную систему.
Сложность экосистемы и найма. Языки с мощными типами — чаще всего нишевые; у них меньше библиотек, меньше специалистов на рынке, меньше готовых решений «из коробки». Для стартапа или продукта сжимающимися сроками это может оказаться дороже, чем выигрыш от безопасности.
Время компиляции. Мощные системы типов, особенно с зависимыми типами и type-level вычислениями, замедляют сборку. В больших кодовых базах цикл «правка → компиляция» может стать узким местом, снижая тот самый quick feedback, ради которого подход и затевался.
Иллюзия полной корректности. «Если компилируется — значит правильно» — сильное, но опасное преувеличение. Компилятор проверяет соответствие типам, а не соответствие бизнес-требованиям. Типы можно спроектировать неверно, и тогда компилируется в корне неправильная программа. Доверие к компилятору не должно заменять понимание предметной области.
Когда использовать
- Функциональные стеки с мощными типами. Haskell, F#, OCaml, Scala, Rust — там, где система типов богата, а команда ею владеет, Type-TDD раскрывается естественно и окупает вложения.
- Safety-critical домены. Финансы, авиация, медицина, инфраструктура — везде, где цена дефекта высока, а формальная верификация инвариантов оправдана. Сдвиг багов на компиляцию здесь не удобство, а требование к риску.
- Долгоживущие кодовые базы со сложной доменной моделью. Если модель будет меняться годами, инвестиция в точные типы окупается: модель становится самодокументируемой и устойчивой к регрессиям. Перекликается с DDD — там домен ведёт архитектуру, здесь типы ведут модель.
- Системы с богатым набором состояний и переходов. Конечные автоматы, протоколы, процессы — класс задач, где «непредставимые состояния» дают максимальную отдачу: некорректный переход просто не компилируется.
- Команды с FP-экспертизой. Там, где навык уже есть, внедрение проходит без болезненной адаптации и даёт быстрый эффект.
- Проекты под AI-генерацию кода. Точные типы служат контрактом для AI-агентов: чем строже сигнатура, тем меньше свободы у модели ошибиться, а компилятор работает автоматическим gate-ом. Подробнее — в AIDD.
- Домены с высокими требованиями к прослеживаемости и аудиту. Где важно показать, что инварианты проверены формально, типизированные контракты дают документируемый след, в отличие от размытых по рантайму проверок.
Когда НЕ использовать
- Динамические языки без строгой типизации. Python, Ruby, JavaScript без TypeScript — здесь нет выразительной системы типов, и попытки имитировать Type-TDD (через аннотации и runtime-проверки) дают тень подхода без его главного эффекта. Лучше работать в парадигме, естественной для стека.
- Одноразовые прототипы и spike-исследования. Если цель — проверить гипотезу и выбросить код, инвестиция в точные типы — чистые убытки. Здесь уместен подход «сделать быстро», а к production-реализации вернуться осознанно.
- Команды без FP-экспертизы и без времени на обучение. Внедрение Type-TDD «с понедельника» в команде без опыта даёт сложный, нечитаемый код и разочарование. Сначала — обучение и пилот на изолированном модуле; потом — масштабирование, если пилот подтвердил ценность.
- Жёсткие «горящие» дедлайны. В режиме deadline-driven затраты на проектирование точных типов первыми отбрасываются как «лишние». Это симптом больного процесса, но реалистично: в таком режиме подход не приживётся, пока не устранена первопричина спешки.
- Домены, где основная сложность — не в инвариантах, а в интеграции и UI. Если главные риски лежат в вёрстке, UX, связи с нестабильными внешними системами, типы мало помогут — там работают другие практики (визуальное тестирование, контрактное тестирование интеграций).
- Начальные стадии стартапа с быстрой сменой гипотез. Пока модель предметной области не устоялась, точные типы приходится переписывать вслед за каждым поворотом понимания. Выгоднее подойти к строгим типам позже, когда ядро стабилизируется.
Связанные подходы
Type-TDD — один из материалов серии о driven-подходах; за разными аббревиатурами стоят разные «движущие силы» (типы, тесты, поведение, домен и т. д.). Ниже — карта родственных подходов со ссылками на опубликованные статьи базы знаний.
Сравнительная таблица: Type-TDD vs Test-TDD
Поскольку дизамбигуация этих двух подходов — критическая, сведём различия в отдельную таблицу.
| Ось сравнения | Type Driven Development | Test Driven Development |
|---|---|---|
| Движущая сила | Типы (система типов языка) | Тесты (исполняемая спецификация) |
| Что проектируется первым | Типы и модель данных | Тест и интерфейс вызова |
| Сигнал «не готово» (красный) | Код не компилируется | Тест падает |
| Сигнал «готово» (зелёный) | Код компилируется | Все тесты проходят |
| Что верифицируется | Структура, инварианты, невозможные состояния | Поведение: вход → ожидаемый выход |
| Где ловится дефект | На этапе компиляции (до запуска) | На этапе запуска тестов (CI/локально) |
| Применимость | Только языки с мощными типами | Любой язык |
| Порог входа | Высокий (FP и продвинутые типы) | Средний (дисциплина и навык тестов) |
| Что НЕ ловится | Бизнес-логика, конкретные значения, IO | Структурные ошибки, о которых не написали тест |
| Стоимость | Долгое проектирование типов, медленнее старт | Накладные расходы на тесты, поддержка набора |
Практический вывод: подходы ортогональны и комплементарны. Типы страхуют структуру и инварианты; тесты — поведение и значения. Зрелая кодовая база на функциональном стеке использует оба слоя: Type-TDD для модели, Test-TDD для бизнес-логики.
Карта driven-подходов
- TDD — Test Driven Development — та же аббревиатура, но движущая сила — тесты как исполняемая спецификация. Ближайший «конкурент» по названию и главный объект дизамбигуации. Дополняет Type-TDD: тесты страхуют то, что типами не выразить.
- DDD — Domain Driven Design — домен как движущая сила. Type-TDD часто служит техническим инструментом реализации DDD: точные типы делают доменную модель явной и проверяемой компилятором.
- BDD — Behaviour Driven Development — поведение на общем языке в сценариях Given-When-Then. Живёт на уровне требований, ортогонален Type-TDD (который работает на уровне кода).
- FDD — Feature Driven Development — клиенто-ориентированная фича как единица организации работы. Организационная методология; Type-TDD — инженерная практика; они не пересекаются по уровню и совместимы.
- MDD — Model Driven Development — формальная модель как источник истины для кодогенерации. Близок по духу «модель ведёт код», но модель там — отдельный артефакт, а в Type-TDD моделью служат сами типы в коде.
- ATDD — Acceptance Test Driven Development — критерии приёмки на уровне требований. Как и BDD, работает выше уровня кода и не конкурирует с Type-TDD.
- HDD — Hypothesis Driven Development — гипотеза как движущая сила продуктового развития. Управленческо-продуктовая практика, ортогональная инженерным подходам.
Смежные темы архитектуры
- Паттерны проектирования — каталог структурных решений; многие из них (Optional, Result, State, Strategy) в Type-TDD приобретают типобезопасное воплощение.
- ACID — транзакционные гарантии как инварианты; концептуальный аналог того, как Type-TDD обеспечивает инварианты на уровне типов, только в области данных.
- Безопасная архитектура — точные типы снижают поверхность атаки: некорректный ввод отбрасывается на границе, а не разгуливает по ядру.
- ADR — фиксация архитектурных решений, в том числе о выборе языка и системы типов ради Type-TDD.
Родственные «сатирические» варианты (Panic Driven Development и подобные) и общий обзор driven-подходов рассматриваются в отдельных статьях серии.
Краткий вердикт для руководителя
Type-TDD — это инвестиция в структурную корректность кодовой базы: сдвиг целого класса дефектов с рантайма на этап компиляции, самодокументирующиеся инварианты и безопасный рефакторинг модели. Эффект: меньше регрессий по состояниям, короче доменное ядро (проверки переезжают на границу), выше устойчивость к изменениям. Цена: жёсткая привязка к языку с мощными типами, высокий порог входа для команды, суженная воронка найма и риск «типов ради типов» при недостатке опыта. Берите, если стек уже функциональный, домен safety-critical или модель данных сложна и долгоживуща, а команда владеет продвинутой типизацией или готова её освоить. Не берите, если язык динамический, горизонт — прототип на недели, а команда не имеет FP-фона и не располагает временем на обучение. И главное: Type-TDD не отменяет тесты — он снимает с них нагрузку по структурным проверкам, оставляя бизнес-логику и эффекты ввода-вывода в полноценной зоне ответственности Test-TDD. Оценивать подход на горизонте одного спринта — ошибка; эффект проявляется на горизонте месяцев, по мере того как модель «обрастает» точными типами.
Источники и материалы
Первоисточники и ключевые публикации
- Edwin Brady. Type-Driven Development with Idris. Manning, 2017 — фундаментальный первоисточник, формулировка цикла «тип → реализация → уточнение» и духа методологии.
- Yaron Minsky. Effective ML (доклад, Jane Street) — каноническая формулировка «make illegal states unrepresentable»; практическое введение в проектирование от типов.
- Alexis King. Parse, don’t validate (блог, 2019) — формулировка принципа переноса проверки на границу системы через типы.
- Mark Seemann. Type-Driven Development (ploeh.dk) — серия эссе о практическом применении подхода в F# и других языках.
- Conor McBride и др. — работы по зависным типам (dependent types) и соответствию Карри — Ховарда; теоретическая база, на которую опирается методология.
- Bertrand Meyer. Object-Oriented Software Construction (1988) — Design by Contract, классическая идея, которую Type-TDD реализует через типы.
Связанные материалы базы знаний
- TDD — Test Driven Development — та же аббревиатура, тестовая методология; главный объект дизамбигуации и естественный комплемент Type-TDD.
- DDD — Domain Driven Design — домен ведёт архитектуру; Type-TDD служит техническим инструментом реализации DDD.
- AIDD — типы как контракт для AI-генерации и автоматический quality-gate.
- Паттерны проектирования — структурные решения, получающие типобезопасное воплощение в Type-TDD.
- ACID — инварианты транзакций как концептуальный аналог инвариантов типов.
- ADR — фиксация решения о выборе языка и системы типов.