Получить тип объединения рекурсивного свойства в TypeScript

Допустим, у нас есть интерфейс:

interface Node<C extends Node[] = any[]> {
    children: C
}

Здесь C - это общий кортеж, являющийся типом дочерних узлов этого узла.

Определим несколько узлов:

type Foo = Node<[]>
type Bar = Node<[Foo, Foo]>
type Baz = Node<[Bar]>

Baz - корневой узел. Он является родителем одного узла Bar, который является родителем двух узлов Foo. У Фу нет детей.

Если я хочу получить потомков узла, я могу:

type TupleOfNodeChildren<N extends Node> = N['children'];

Вот несколько примеров этого TupleOfNodeChildren типа, который работает должным образом:

type T0 = TupleOfNodeChildren<Foo> // []
type T1 = TupleOfNodeChildren<Bar> // [Foo, Foo]
type T3 = TupleOfNodeChildren<Baz> // [Bar]

Теперь предположим, что мне нужен тип, представляющий собой объединение всех типов в кортеже. Я могу сделать:

type TypesOfNodeChildren<N extends Node> = TupleOfNodeChildren<N>[number];

Ну и конечно же наши примеры:

type T10 = TypesOfNodeChildren<Foo> // never
type T11 = TypesOfNodeChildren<Bar> // Foo
type T12 = TypesOfNodeChildren<Baz> // Bar

Все это прекрасно работает. Но что, если мне нужно что-то под названием TypesOfAllChildren, что похоже на TypesOfNodeChildren, но вместо того, чтобы быть просто объединением непосредственных дочерних элементов, это объединение всех дочерних узлов узла?

Вот как это будет работать:

type T20 = TypesOfAllChildren<Foo> // never
type T21 = TypesOfAllChildren<Bar> // Foo
type T22 = TypesOfAllChildren<Baz> // Bar | Foo    <--- Includes types of deep children

Обратите внимание, что T22 имеет как Bar, непосредственный дочерний элемент Baz, так и Foo, который является дочерним элементом Bar.

Кажется, я не могу заставить работать этот TypesOfAllChildren тип; он продолжает жаловаться на циклическую ссылку, что бы я ни пытался. Я предполагаю, что вам нужна какая-то рекурсия, чтобы получить типы всех дочерних элементов, но я не уверен, как реализовать это без жалоб TypeScript. Здесь есть детская площадка с этими типами и примерами.

ИЗМЕНИТЬ:

Вот пример того, что я пробовал:

type TypesOfAllChildren<N extends Node> = TypesOfNodeChildren<N> | TypesOfAllChildren<TypesOfNodeChildren<N>>;
//   ~~~~~~~~~~~~~~~~~~ Recursively references itself

Добавление условия выхода через условный тип также не работает:

type TypesOfAllChildren<N extends Node> = TypesOfNodeChildren<N> | (TypesOfNodeChildren<N> extends never ? never : TypesOfAllChildren<TypesOfNodeChildren<N>>);

person Splox    schedule 29.06.2020    source источник


Ответы (1)


Я решил это. Итак, у нас есть:

interface Node<C extends Node[] = any[]> {
    children: C
}

type Foo = Node<[]>
type Bar = Node<[Foo, Foo]>
type Baz = Node<[Bar]>
type Faz = Node<[Baz]>

И хотите иметь тип, который может объединить всех дочерних элементов повсюду в дереве. Я обнаружил, что это работает:

type SelfAndAllChildren<N extends Node> = N['children'][number] extends never ? N : {
    // @ts-ignore
    getSelfAndChildren: N | SelfAndAllChildren<N['children'][number]>
}[N extends never ? never : 'getSelfAndChildren']

Итак, сначала:

  1. Существует условие, которое проверяет, является ли тип детей never, т.е. у узла нет детей. Если это так, мы просто возвращаем N и готово.
  2. Если есть дочерние элементы, то сначала мы создаем тип, содержащий getSelfAndChildren. Нам нужно это сделать, потому что только типы свойств могут рекурсивно ссылаться на себя - типы объединения не могут.
  3. Свойство getSelfAndChildren будет включать сам узел, N, а затем SelfAndAllChildren дочерних узлов. Это рекурсивная ссылка на SelfAndAllChildren, но поскольку это свойство getSelfAndChildren, TypeScript не жалуется.
  4. Наконец, нам нужно получить тип свойства getSelfAndChildren. К сожалению, TypeScript будет жаловаться на рекурсивную ссылку , если мы не используем условный тип для индексации интерфейса с getSelfAndChildren в нем. Итак, мы добавляем условие, которое всегда будет возвращать тип getSelfAndChildren, который сам имеет рекурсивную ссылку на SelfAndAllChildren.
  5. Обратите внимание на @ts-ignore над getSelfAndChildren. Если бы мы передали any в SelfAndAllChildren, это привело бы к ошибке. Установка @ts-ignore заглушает ошибку на any, а тип по умолчанию - any, что мы и хотим.

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

Кроме того, вот тип, который просто получает детей, если вы это искали:

type JustAllChildren<N extends Node> = SelfAndAllChildren<N['children'][number]>

А затем здесь есть детская площадка со всем.

person Splox    schedule 29.06.2020