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