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. Первые результаты могут потребовать времени, но это тот самый путь, который отличает среднего разработчика от эксперта, способного проектировать системы, а не просто их описывать.

Picture of Роман

Роман

автор-эксперт

Популярные статьи по теме

Курсы по кибербезопасности (White Hat): с нуля до пентестера — обзор цен

Курсы по кибербезопасности (White Hat): с нуля до пентестера — обзор цен

Как пройти путь от новичка до пентестера и не переплатить за курсы по кибербезопасности Вы хотите сменить профессию и видите ...
DevOps-инженер: подборка интенсивов от экспертов рынка

DevOps-инженер: подборка интенсивов от экспертов рынка

DevOps-инженер: как выбрать интенсив, который точно окупится в 2026 Вы тратите вечера на поиск подходящих курсов для DevOps-инженера, но итог ...
Импортозамещение в IT: курсы по 1С, 1С-Битрикс и отечественным платформам

Импортозамещение в IT: курсы по 1С, 1С-Битрикс и отечественным платформам

Как построить карьеру в IT на волне импортозамещения: реалистичный план для 2026 Если вы следите за рынком IT-услуг в России, ...
Python-разработчик в 2026: roadmap и лучшие курсы от нуля до трудоустройства

Python-разработчик в 2026: roadmap и лучшие курсы от нуля до трудоустройства

Как стать python-разработчиком в 2026: реалистичный путь от выбора курса до первой работы Вы хотите изучить Python, но вас ошеломила ...
Цифровые профессии со 100% компенсацией от государства: проект «Содействие занятости»

Цифровые профессии со 100% компенсацией от государства: проект «Содействие занятости»

Бесплатная переквалификация в IT: как получить профессию за 0 рублей от государства в 2026 году Стоимость курсов по Data Science ...
Социальный вычет через работодателя: получаем деньги за обучение сразу, а не через год

Социальный вычет через работодателя: получаем деньги за обучение сразу, а не через год

Получить деньги за обучение сейчас, а не ждать до следующего года Вы платите за свою учебу, курсы или образование ребенка, ...