Условные типы в 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 по форме или манипуляция кортежами. Это даст прикладное понимание и откроет новые, полезные паттерны в повседневной работе с типами.

