TypeScript на максимум: как Infer и вариативные типы решают реальные задачи
Вы пишете универсальный тип для библиотеки или фреймворка. Вы понимаете логику, но TypeScript выдает невразумительную ошибку: “Type ‘string’ is not assignable to type ‘never’”. Вы тратите часы, пытаясь заставить код работать с условными типами, но выхлоп — хрупкие конструкции, которые ломаются при первом же расширении. Проблема не в вашем понимании, а в отсутствии владения двумя инструментами: оператором infer и вариативными типами (variadic tuple types). Их комбинация — это не синтаксический сахар, а фундаментальный сдвиг в проектировании сложных типов. Эта статья покажет вам, как именно.
Вариативные типы: управление кортежами как массивом
До TypeScript 4.0 кортеж был жесткой структурой. Тип [string, number] не позволял работать с произвольным количеством элементов. Вариативные типы изменили правила игры. Они используют синтаксис распространения ... в типах, позволяя моделировать кортежи переменной длины с сохранением информации о каждом элементе.
Ключевое отличие от массива: [...string[]] — это бесконечный однородный массив строк. [string, ...number[]] — это вариативный кортеж, где первый элемент точно строка, а за ним следует любое количество чисел. Эта точность критична для функций вроде bind или curry.
Реальный сценарий: тип для функции compose
Напишем тип для функции композиции, которая принимает N функций и возвращает новую функцию. Без вариативных типов это было бы костылем с потерей информации о типах аргументов.
type Compose<T extends any[]> = (
...funcs: { [K in keyof T]: (arg: any) => T[K] }
) => (initialArg: any) => T extends [infer First, ...infer Rest] ? First : never;
// Этот тип слаб. Мы теряем информацию о цепочке типов.
Теперь с вариативными типами и infer:
type Compose<Fns extends ((arg: any) => any)[]> =
Fns extends [(arg: infer A) => infer B]
? (arg: A) => B
: Fns extends [...infer Rest, (arg: infer Penultimate) => infer Last, (arg: infer Arg) => Penultimate]
? (arg: Arg) => Last & Compose<Rest>
: never;
function compose<Fns extends ((arg: any) => any)[]>(...funcs: Fns): Compose<Fns> {
return funcs.reduce((prev, curr) => (arg: any) => curr(prev(arg)));
}
Здесь вариативный тип Fns захватывает весь кортеж функций. Оператор infer внутри условного типа декомпозирует этот кортеж: он вытаскивает типы аргументов и возвращаемых значений из последней и предпоследней функций, рекурсивно строя итоговую сигнатуру. Это тип, который действительно гарантирует безопасность.
Оператор infer: вытаскивание типа из оболочки
Infer работает только в пределах условного типа под ключевым словом extends. Его задача — объявить переменную типа на месте, где вы ожидаете определенную структуру. Это как деструктуризация, но для типов.
Простейший пример: извлечение типа элемента из массива.
type ElementType<T> = T extends (infer U)[] ? U : never;
type Str = ElementType<string[]>; // string
type Num = ElementType<number[]>; // number
Однако его истинная сила раскрывается в паттернах, где структура сложнее.
Распространенная и разрушительная ошибка новичка
Самая частая ошибка — попытка использовать infer вне условного типа или использование нескольких infer в одной ветке без учета их связи.
type BrokenInfer<T> = infer U extends T ? U : T; // Ошибка компиляции!
// 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
Другая ошибка — непонимание, что каждый infer создает новую переменную типа.
type FirstTwo<T> = T extends [infer A, infer B, ...any[]] ? [A, B] : [];
// Это корректно. A и B могут быть разными типами.
Как исправить: Всегда проверяйте, что infer находится внутри T extends <шаблон с infer>. Для вывода связанных типов из одной структуры (например, типа аргумента и возвращаемого значения функции) используйте один шаблон.
type FunctionParamsAndReturn<T> =
T extends (...args: infer Params) => infer Return
? { params: Params; return: Return }
: never;
// Один шаблон, две переменные типа, связанные одной структурой функции.
Комбинация: построение типов для функций с остаточными параметрами
Рассмотрим сложный кейс, актуальный для разработки API в 2026 году. Тенденция — это рост библиотек с плагинной архитектурой, где ядро принимает переменное количество middleware или хуков с четкой последовательностью типов. Вам нужно описать тип функции createPipeline, которая принимает массив функций-обработчиков. Каждая последующая функция получает на вход результат предыдущей плюс дополнительный контекст, накапливаемый по цепочке.
Note: В этом сценарии мы не можем использовать простой Array<Function>. Нам критически важно сохранить информацию о типе каждого звена цепи, чтобы итоговая функция была строго типизирована.
Задача: написать тип Pipeline<Fns>, который из кортежа функций Fns выведет тип итоговой функции, правильно передавая накопленный контекст.
type Accumulator = { context: Record<string, unknown> };
type PipelineStep<Input, Output> = (input: Input, acc: Accumulator) => Output;
type Pipeline<Fns extends PipelineStep<any, any>[]> =
Fns extends []
? (acc: Accumulator) => Accumulator
: Fns extends [PipelineStep<infer Input, infer Output>]
? (input: Input, acc: Accumulator) => Output
: Fns extends [PipelineStep<infer FirstInput, infer FirstOutput>, ...infer Rest]
? Rest extends PipelineStep<any, any>[]
? (input: FirstInput, acc: Accumulator) => Pipeline<[PipelineStep<FirstOutput, any>, ...Rest]>
: never
: never;
Разберем логику. Первый условный тип обрабатывает пустой массив. Второй — массив из одной функции. Третий и самый важный — рекурсивный случай. Сначала с помощью infer извлекаем типы первой функции. Затем, используя вариативный оператор ...infer Rest, захватываем оставшийся кортеж. Ключевой трюк — проверка Rest extends PipelineStep<any, any>[]. Она нужна, чтобы TypeScript понял, что Rest — это кортеж функций, и мы можем рекурсивно применить к нему тип Pipeline, сшивая вход и выход по цепочке.
Вопрос и ответ (FAQ)
Вопрос: Почему иногда TypeScript выводит тип unknown вместо ожидаемого, когда я использую infer в сложном условном типе?
Ответ: Это происходит, когда TypeScript не может с уверенностью сопоставить шаблон. Убедитесь, что ваше условие extends достаточно конкретно. Часто помогает добавление дополнительных ограничений (constraints) на дженерики выше по цепочке или разбивка одного сложного условного типа на несколько вложенных, где каждый шаг проверяет более простую структуру.
Практический аудит вашего кода
Где искать точки применения этих знаний в существующем проекте?
- Перепишите перегрузки функций (function overloads). Если у вас более трех перегрузок для одной функции, вероятно, их можно заменить одним типом с infer и вариативным кортежом.
- Проверьте утилитарные типы. Используете ли вы самописные
PromiseType,FunctionArgs? Замените их на версии с infer для большей надежности. - Анализируйте типы в API роутера или状态-менеджере. Места, где функции-обработчики объединяются в цепочки (middleware, enhancers) — главные кандидаты для рефакторинга с вариативными типами.
Эти инструменты — infer, вариативные кортежи, рекурсивные типы — перестают быть “продвинутыми темами”. В экосистеме TypeScript 2025-2026 они становятся стандартом для написания устойчивых, выразительных и безопасных типов для библиотек и сложной бизнес-логики. Их освоение напрямую влияет на скорость разработки и количество runtime-ошибок.
Откройте ваш основной проект. Найдите одну функцию с более чем двумя перегрузками или любой тип, использующий any[] для описания кортежей. Попробуйте переписать его, используя [...Type[]] и infer. Первые результаты могут потребовать времени, но это тот самый путь, который отличает среднего разработчика от эксперта, способного проектировать системы, а не просто их описывать.

















