Home > Enterprise >  Typescript: how to generate Tuple out of Array Tree recursively?
Typescript: how to generate Tuple out of Array Tree recursively?

Time:05-10

I have the following data structure:

const data = [{
   value: 'value',
   label: 'Label',
   children: [
    {
       value: 'value.1',
       label: 'Label.1',
       children: [{
         value: 'value.1.1',
         label: 'Label.1.1',
       }],
    }, 
    {
       value: 'value.2',
       label: 'Label.2',
       children: [{
         value: 'value.2.1',
         label: 'Label.2.1',
       }],
    }
   ],
},{
   value: 'value2',
   label: 'Label2',
}] as const;

My aim is to generate the following tuple base on data with (editor) auto-complete:

[parent, children, childrenOfChildren]

with:

parent = 'value' | 'value2'; 
children = if parent === 'value' ? 'value.1' | 'value.2'; // The comment below explain the flow
// ['value' | 'value2'] > 'value' > ['value', 'value.1' | 'value.2'] > 'value.1' > ['value', 'value.1', 'value.1.1' | 'value.1.2'] > 'value.1.1' > ['value', 'value.1', 'value.1.1']
...

So the tuple should be dynamic based on the depth of the children array in the object representing the key in data. Picking 'value' will look something like this:

['value' | 'value2', 'value.1' | 'value.2' ] where 'value.1' | 'value.2' can be chosed from.

Picking 'value' and then 'value.1' will be:

['value', 'value.1', or (value.1).children.value ].

What I've done so far:

  1. Convert data to const: data = [...] as const;
  2. Generic type
declare const generateTuple: <T extends Record<K, PropertyKey | T[]>, K extends keyof T>(
  objArray: readonly T[],
  property: K,
) => [T[K]];

This successfully generates type for the given key, but not for the depth. I couldn't find a way to generate the return tuple type recursively:

[T[K], generateTuple(T[K]['children'], k)]

enter image description here

CodePudding user response:

My interpretation is that you have a value data of type extending Data as defined by:

type Data = readonly DataElement[];

interface DataElement {
  value: string;
  label: string;
  children?: Data;
}

And you'd like to define a type function like type DataPaths<T extends Data> that evaluates to a union of tuple types representing every possible path of value properties down through the tree. For data as given in your question, this should look something like:

type ValidPaths = DataPaths<typeof data>
// type ValidPaths = [] | ["value"] | ["value", "value.1"] | 
// ["value", "value.1", "value.1.1"] | ["value", "value.2"] | 
// ["value", "value.2", "value.2.1"] | ["value2"]

It's not clear to me whether you actually only want paths that go all the way down to the leaf nodes of the tree structure, or if you're okay with paths that end before the leaves. I'm going with the latter, so that's why the above includes ["value"] as well as the empty tuple [].


Here's one way to implement it:

type DataPaths<T extends Data | undefined> = 
  [T] extends [Data] ? DataElementPaths<T[number]> : []

type DataElementPaths<T extends DataElement> = [] | (
  T extends DataElement ? [T["value"], ...DataPaths<T["children"]>] : never
)

The idea is that DataPaths acts upon a (possibly undefined) array of DataElement types, while DataElement acts upon a DataElement type (or a union of them). The DataPaths<T> type is implemented to either return DataElementPaths for all array elements of T if it's an array or the empty tuple otherwise (if it's undefined).

And DataElementPaths just prepends (via variadic tuple types) the value property of the DataElement to the result of recursively evaluating DataPaths for the children property.

Note that the empty tuple is unioned into the result of DataElementPaths no matter what (as [] | ); it is this which allows partial paths; if you removed that then you'd only get paths to leaves.

And also note that in order for unions of inputs to become unions of outputs, this is implemented as a distributive conditional type; hence the T extends DataElement ? ... : never. It seems like a no-op (we already know that T extends DataElement) but without it you'd only get a single tuple and not a union out.


And you can verify that the implementation does produce the desired type for ValidPaths.

Playground link to code

  • Related