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

В этой статье я расскажу о том, как условные конструкции работают с ключевым словом infer, какие паттерны встречаются чаще всего и где они действительно помогают в реальной разработке. Постараюсь дать конкретные примеры и объяснить, почему порой один тип ставит «мостик» между runtime и compile-time логикой.

Что такое условные типы: простая интуиция

Условный тип в TypeScript имеет форму A extends B ? X : Y и вычисляет результат в зависимости от совместимости A с B. На первый взгляд это напоминает if в обычном коде, но сравнение происходит на уровне типов, а результатом служит новый тип.

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

Распространённые операции с extends

Самая тривиальная задача — проверка, является ли тип подмножеством другого. Например, проверка на функцию или на объект с конкретным свойством. Такие проверки используются для защиты общих утилит и составления безопасных обёрток.

Ещё одно практическое применение — разрешение неоднозначностей: условный тип может вернуть never для неподходящих вариантов и тем самым исключить их из дальнейших вычислений. Этот приём часто встречается в библиотеках типов.

Как распределение по union облегчает жизнь

Распределение по union означает, что условный тип T extends U ? X : Y для T = A | B превращается в (A extends U ? X : Y) | (B extends U ? X : Y). Это даёт возможность создавать селекторы по форме типов — например, отбирать только те элементы union, которые удовлетворяют условию.

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

Ключевое слово infer: извлечение внутренних частей

Инструкция infer позволяет внутри условного типа объявить новую переменную типа и затем вернуть её. По сути мы говорим компилятору: «Попробуй вывести часть типа и используй её дальше». Это похоже на pattern matching, но на уровне типов.

Типичный пример — извлечь тип возвращаемого значения функции. Вместо ручного перечисления сигнатур мы можем написать правило, которое подставит нужную часть автоматически. Такой подход делает определения модульными и легко поддерживаемыми.

Пример: извлечение типа возвращаемого значения

Ниже простой шаблон, который берёт тип функции и возвращает её возвращаемый тип с помощью infer. Он служит основой для встроенного утилитарного типа ReturnType.

type MyReturn = F extends (...args: any[]) => infer R ? R : never;

Если F — функция, внутри условного типа будет создана переменная R, соответствующая возвращаемому значению. В противном случае результат — never, что удобно для дальнейшей фильтрации.

Infer с кортежами и rest-параметрами

Infer работает не только с функциями. Его можно применять к кортежам, чтобы вытащить последний элемент, хвост или первые элементы. Такой приём часто используют при построении типов для операций над массивами и аргументами.

type Head = T extends [infer H, ...any[]] ? H : never;
type Tail = T extends [any, ...infer R] ? R : never;

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

Практические утилиты и паттерны

На основе условных типов и infer реализованы многие встроенные утилиты: ReturnType, Parameters, ConstructorParameters, InstanceType и другие. Понимание их реализации помогает создавать собственные, более гибкие абстракции.

Ниже несколько часто встречающихся паттернов, которые удобно держать в голове:

  • Извлечение частей функции: аргументы, возвращаемое значение, this.

  • Работа с кортежами: head, tail, prepend, append.

  • Фильтрация по форме: оставить из union только функции, строки или объекты с нужными полями.

Примеры из реальной практики

В одном из проектов мне приходилось писать API-обёртку, где нужно было сохранять строгие типы для ответов сервера, но при этом допускать разные формы запроса. Я использовал условные типы, чтобы для разных endpoint возвращать соответствующие типы данных без ручной расписи широкого количества overload’ов.

Другой случай — библиотека компонентов. Я сделал универсальную типизацию для props: общие поля проверялись отдельным условным типом, а специфичные извлекались через infer из компонентных деклараций. Это сократило количество шаблонного кода и снизило количество ошибок на compile-time.

Подводные камни и ловушки

Условные типы могут неожиданно «распыляться» при сложных объединениях, приводя к очень длинным результирующим типам или неожиданным never. Важно тестировать промежуточные шаги и по возможности ограничивать union’ы.

Ещё одна проблема — производительность компилятора. Глубокие рекурсивные условные типы и большое число infer-вычислений увеличивают время компиляции. В таких случаях полезно упростить типы или разделить их на более явные куски.

Советы по отладке и поддержке типов

Если тип ведёт себя не так, как ожидалось, хороший приём — вынести часть условного типа в отдельный alias и посмотреть, какой результат он даёт. Это делает выводы компилятора более читаемыми и упрощает поиск ошибки.

Другой подход — использовать тесты типов с помощью небольших утверждений: проверять, что полученный тип совместим с ожидаемым. Это особенно полезно в больших кодовых базах, где изменение одной утилиты может повлиять на множество мест.

Короткая таблица: поведение при разных входных типах

Вход Пример Поведение
Функция (…args: any[]) => string infer может извлечь возвращаемый тип или аргументы
Кортеж [number, string] infer извлечёт элементы по позициям или хвост
Union A | B условие распределится по каждому члену union

Когда не стоит злоупотреблять условными типами

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

Нужно также учитывать совместимость с инструментами: автодополнение и сообщения об ошибках должны оставаться полезными. Слишком хитро сконструированные типы иногда делают разработку менее предсказуемой.

Заключительная мысль без штампов

Условные типы и infer превращают типовую систему TypeScript в рабочий инструмент, позволяющий выражать сложную логику на этапе компиляции. Они не заменяют здравый смысл, но помогают сократить количество runtime-проверок и получить более точные контракты между частями кода.

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