0Pricing
TypeScript Academy · Урок

Рекурсия на уровне типов

Обходите типы с помощью рекурсивных условных типов

«Рекурсия на уровне типов» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Типы как язык вычислений
  2. Условные типы на уровне типов
  3. Рекурсия на уровне типов
  4. Распределяемые условные типы
← Назад к TypeScript Academy