Рекурсия на уровне типов
Обходите типы с помощью рекурсивных условных типов
«Рекурсия на уровне типов» — бесплатный урок TypeScript Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения TypeScript Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс TypeScript Academy содержит 4 уроков всего.
Рекурсия в типах
Условный тип может ссылаться на самого себя. Это добавляет циклы в язык типов. Большинство рекурсивных операций на уровне типов проходит по кортежу по одному элементу, отделяя первый элемент и рекурсивно обрабатывая хвост.
type Length<T extends unknown[]> =
T extends [unknown, ...infer Rest]
? Length<Rest>
: 0;
// (this counts down to a base case)Базовый случай
Каждой рекурсии нужно условие остановки. Для кортежей это обычно пустой кортеж. Когда шаблон [head, ...rest] больше не совпадает, значит, достигнут конец и можно вернуть фиксированный результат.
type IsEmpty<T extends unknown[]> =
T extends [] ? true : false;
type A = IsEmpty<[]>; // true
type B = IsEmpty<[1, 2]>; // falseГолова и хвост
Основной приём — разделить кортеж на его первый элемент и оставшийся кортеж, используя infer вместе с шаблоном распространения.
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];
type A = Head<[1, 2, 3]>; // 1
type B = Tail<[1, 2, 3]>; // [2, 3]Обработка элемента за элементом
Объедините первый элемент, хвост и рекурсию, чтобы преобразовать весь кортеж. ToStrings заново создаёт кортеж, заменяя тип каждого элемента на string.
type ToStrings<T extends unknown[]> =
T extends [infer H, ...infer R]
? [string, ...ToStrings<R>]
: [];
type A = ToStrings<[1, true, 3]>; // [string, string, string]Шаблон с аккумулятором
Часто результат накапливают в дополнительном параметре, который называется аккумулятором. Изначально он пуст, а на каждом шаге увеличивается. Это позволяет не проходить структуру повторно и является стандартным приёмом для циклов на уровне типов.
type Reverse<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer R]
? Reverse<R, [H, ...Acc]>
: Acc;
type A = Reverse<[1, 2, 3]>; // [3, 2, 1]Пошаговый разбор аккумулятора
Проследим выполнение Reverse<[1,2,3]>:
- Шаг 1: H=1, Acc становится [1]
- Шаг 2: H=2, Acc становится [2,1]
- Шаг 3: H=3, Acc становится [3,2,1]
- Кортеж пуст, возвращается Acc = [3,2,1]
Аккумулятор передаёт ответ вниз при каждом рекурсивном вызове.
type R = Reverse<["a", "b"]>; // ["b", "a"]Рекурсия по объединениям
Рекурсию также можно использовать для объединения строк в одну. Здесь элементы кортежа соединяются в один строковый литеральный тип с разделителем.
Обозначения: в настоящем TypeScript типы шаблонных строк записываются строками в обратных кавычках, содержащими места для подстановки в формате «доллар — фигурные скобки». В этих фрагментах мы показываем такой шаблон как Tpl<...>, перечисляя все его части по порядку; например, шаблон в обратных кавычках, соответствующий сначала литеральному префиксу, а затем Rest, выглядит как Tpl<'prefix', infer Rest>.
type Join<T extends string[], Sep extends string = ','> =
T extends [infer H extends string, ...infer R extends string[]]
? R extends []
? H
: Tpl<H, Sep, Join<R, Sep>>
: '';
// Tpl<H, Sep, ...> builds a template literal type joining the parts
type A = Join<['a', 'b', 'c']>; // 'a,b,c'Подсчёт с помощью рекурсии
Рекурсия вместе с аккумулятором позволяет выполнять подсчёт. Создайте кортеж из unknown той же длины, что и входные данные, а затем прочитайте его length. Этот приём часто используется в курсе арифметики.
type Count<T extends unknown[], Acc extends unknown[] = []> =
T extends [unknown, ...infer R]
? Count<R, [unknown, ...Acc]>
: Acc["length"];
type A = Count<["x", "y", "z"]>; // 3Фильтрация с помощью рекурсии
Пройдите по кортежу и оставьте только элементы, соответствующие условию, удалив остальные. Чтобы пропустить элемент, не добавляйте его в аккумулятор.
type KeepStrings<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer R]
? H extends string
? KeepStrings<R, [...Acc, H]>
: KeepStrings<R, Acc>
: Acc;
type A = KeepStrings<[1, "a", 2, "b"]>; // ["a", "b"]Ограничения глубины рекурсии
Компилятор ограничивает глубину рекурсии (исторически она составляла около 50, хотя оптимизация хвостовой рекурсии позволяет в некоторых шаблонах обрабатывать больше). Для очень больших кортежей можно столкнуться с ошибкой глубина инстанцирования типа превышает допустимую. Используйте хвостовой стиль с аккумулятором, чтобы увеличить этот предел.
type Repeat<T, N extends number, Acc extends T[] = []> =
Acc["length"] extends N ? Acc : Repeat<T, N, [...Acc, T]>;
type A = Repeat<0, 3>; // [0, 0, 0]Собираем всё вместе
Разделение на первый элемент и хвост, базовый случай и аккумулятор — три составляющие почти каждого цикла на уровне типов. С их помощью можно полностью в системе типов отображать, фильтровать, переворачивать, подсчитывать и объединять кортежи.
type MapToPairs<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer R]
? MapToPairs<R, [...Acc, [H, H]]>
: Acc;
type A = MapToPairs<[1, 2]>; // [[1, 1], [2, 2]]Быстрая проверка
Проверьте, насколько хорошо Вы понимаете шаблон с аккумулятором.
Итоги
Теперь Вы умеете создавать циклы на уровне типов.
- Самоссылочные условные типы создают рекурсию.
- Разделяйте кортежи на
[H, ...R]и рекурсивно обрабатывайте хвост. - Базовый случай (пустой кортеж) останавливает цикл.
- Аккумулятор создаёт результат и позволяет выполнять глубокую рекурсию.
Далее: особое поведение условных типов для объединений.
Часто задаваемые вопросы
Урок «Рекурсия на уровне типов» бесплатный?
Да — полный текст урока «Рекурсия на уровне типов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс TypeScript Academy, подпишись на CoddyKit PRO. Курс TypeScript Academy содержит 4 уроков всего.
Чему я научусь в уроке «Рекурсия на уровне типов»?
Обходите типы с помощью рекурсивных условных типов Ты практикуешь TypeScript Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать TypeScript Academy?
Предыдущий опыт не требуется. TypeScript Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Рекурсия на уровне типов»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке TypeScript Academy?
Да. Каждый урок TypeScript Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Типы как язык вычислений
- Условные типы на уровне типов
- Рекурсия на уровне типов
- Распределяемые условные типы