Typescript приносит нам замечательные вещи, которые упрощают нашу повседневную жизнь. Мы можем лучше спать по ночам, потому что Typescript проверяет типы и защищает нас от ошибок во время выполнения.

Сегодня я хочу поговорить о рекурсивных структурах данных в Typescript. Вот базовый пример рекурсии в типах:

type Node = {
 value: number;
 children: Node[];
};

Мы видим, что тип Node указывает на себя и корневой объект может иметь бесконечную глубину. Даже если мы изменим тип на что-то вроде:

type Node = {
  value: number;
  children?: Node[];
};

Мы видим, что количество уровней по-прежнему не ограничено, и мы можем добавить столько уровней, сколько захотим.

Почему мы должны заботиться

В большинстве случаев нас это вообще не волнует. Но может возникнуть ситуация, когда нам нужно ограничить глубину.

В моем случае мне нужно было создать вложенное поле выбора в моем проекте, а глубина должна быть ограничена 4 уровнями. Опять же, имеет смысл ограничить глубину, но помимо этого я использовал форму реакции-хука в качестве инструмента управления состоянием формы, и это также не допускает рекурсивных типов в состоянии формы.

Так что для меня причин две:

  • Чтобы выразить ограничения, которые у нас есть на уровне типов (не только на уровне кода);
  • Удовлетворить условиям внешних библиотек.

Попробуем реализовать

Решение проблемы не кажется очень сложным, верно. Попробуйте сделать ограничения общими, прежде чем читать дальше :)

Итак, вот код моей версии generic:

export type LimitRecursion<
  T extends {},
  K extends keyof T,
  S extends number
> = Modify<
  T,
  {
    [N in keyof T]: N extends K
      ? GreaterThan<S, 0> extends true
        ? LimitRecursion<T, K, Subtract<S, 1>>[]
        : never
      : T[N];
  }
>;

Позвольте мне объяснить, что здесь происходит. Наш дженерик принимает 3 аргумента:

  • T — рекурсивный тип, мы хотим ограничить
  • K — ключ рекурсивного типа, это свойство, которое мы хотим ограничить
  • S означает предел, это просто число, которое описывает, сколько уровней наша рекурсия должна иметь в конце.

Это все еще не кажется слишком сложным, не так ли?

Но вы, наверное, заметили, что используются некоторые математические дженерики, которые по умолчанию не поддерживаются в Typescript. Давайте напишем их сейчас?

export type GreaterThan<
  A extends number,
  B extends number,
  S extends any[] = []
> = S["length"] extends A
  ? false
  : S["length"] extends B
  ? true
  : GreaterThan<A, B, [...S, any]>;
export type LessThan<
  A extends number,
  B extends number,
  S extends any[] = []
> = S["length"] extends B
  ? false
  : S["length"] extends A
  ? true
  : LessThan<A, B, [...S, any]>;
export type Subtract<
  A extends number,
  B extends number,
  I extends any[] = [],
  O extends any[] = []
> = LessThan<A, B> extends true
  ? never
  : LessThan<I["length"], A> extends true
  ? Subtract<
      A,
      B,
      [...I, any],
      LessThan<I["length"], B> extends true ? O : [...O, any]
    >
  : O["length"];

Надеюсь, реализация математических дженериков вам более-менее понятна. Если нет, дайте мне знать в комментариях, чтобы я мог подготовить более подробную информацию о математике в Typescript в своих следующих статьях.

Есть еще одна вещь, которую мы упустили — Modify generic. Это довольно просто. Он принимает 2 объекта и заменяет свойства первого объекта перекрывающимися свойствами второго. Вот реализация:

type Modify<T, E> = Omit<T, keyof E> & E;

Итак, теперь мы готовы использовать наш дженерик. Давайте используем его с нашим типом Node, который мы реализовали в начале статьи:

export type LimitedNodesTree = LimitRecursion<Node, "children", 2>;

Надеюсь, статья была для вас интересна и математические операции в Typescript, а также рекурсивные типы теперь вам понятнее.

Вы можете прочитать все решение в моем codesandbox: https://codesandbox.io/s/limitrecursion-q1046v