Splox
Splox

Reputation: 761

Get union type of recursive property in TypeScript

Suppose we have the interface:

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

Here, C is a generic that's a tuple that is the type of this node's children.

Let's define some nodes:

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

Baz is the root node. It is the parent of one Bar node, which is the parent of two Foo nodes. Foo has no children.

If I want to get the children of a node, I can do:

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

Here are some examples of this TupleOfNodeChildren type, which works as expected:

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

Now let's say that I want a type that is a union of every different type in the tuple. I can do:

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

And then of course our examples:

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

All of that works nice and fine. But what if I want something called TypesOfAllChildren, which is like TypesOfNodeChildren, but instead of just being a union of the immediate children, it is a union of all of the node's children?

This is how it would work:

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

Notice that T22 has both Bar, the immediate child of Baz, and then also Foo, which is the child of Bar.

I can't seem to get this TypesOfAllChildren type to work; it keeps complaining about a circular reference no matter what I try. I am assuming that you need some kind of recursion to get the types of all of the children, but I am not sure how to implement that without TypeScript complaining. Here is a playground with these types and examples.

EDIT:

Here is an example of what I tried:

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

Adding an exit condition via a conditional type also doesn't work:

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

Upvotes: 4

Views: 1021

Answers (1)

Splox
Splox

Reputation: 761

I solved it. So we have:

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

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

And want to have a type that can get a union of all children, everywhere in the tree. I found that this works:

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

Okay, so first:

  1. There is a conditional that checks if the type of children is never, I.E. the node doesn't have any children. If that is the case, then we just return N and are done.
  2. If there are children, then first we create a type that holds getSelfAndChildren. We need to do this because only the types of properties are allowed to recursively reference themselves -- union types can't.
  3. The getSelfAndChildren property will include the node itself, N, and then the SelfAndAllChildren of the node's children. This is a recursive reference to SelfAndAllChildren, but because it's on the getSelfAndChildren property, TypeScript doesn't complain.
  4. Lastly, we need to actually get the type of the getSelfAndChildren property. Unfortunately, TypeScript will complain about a recursive reference unless we use a conditional type to index the interface with getSelfAndChildren in it. So, we put in a conditional which will always return the type of getSelfAndChildren -- which itself has a recursive reference to SelfAndAllChildren.
  5. Note the @ts-ignore above the getSelfAndChildren. If we were to pass in any to SelfAndAllChildren, that would throw an error. Putting the @ts-ignore silences the error on any, and the type defaults to any, which is what we want.

Please note that this is "not recommended" because of its possible performance impact.

Also, here is a type that just gets the children in case you were looking for that:

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

And then here is a playground with everything.

Upvotes: 1

Related Questions