pilotguy
pilotguy

Reputation: 687

How can I create a recursive type based on object structure?

I have a recursive object structure defined as such:

interface Item {
  name: string;
  type: 'string' | 'url' | 'list' | 'file',
  require?: boolean;
  allowedFileTypes: string[];
  subFields: Item[];
}

const items = {
  name: "navigation",
  type: "list",
  subFields: [
    {
      name: "label",
      type: "string",
      defaultValue: "",
      require: true,
    },
    {
      name: "url",
      type: "url",
      defaultValue: "",
      require: true,
    },
    {
      name: "images",
      type: "list",
      subFields: [
        {
          name: "label",
          type: "string",
          defaultValue: "",
          require: true,
        },
        {
          name: "icon",
          type: "file",
          allowedFileTypes: ["png", "svg"],
        },
      ],
    },
  ],
};

I'm attempting to derive the following from this structure:

// The output would be:
type Items = {
  navigation: Array<{
    label: string;
    url: string;
    images: Array<{
      label: string;
      icon: 'png' | 'svg';
    }>
  }>
}

In a separate question, someone managed to help me use typeof to derive a flat version of this structure but I got stumped on the recursive part with TS. Here's the solution.

Basically, how can I use typeof to recursively derive my nested type based on a defined object as a generic object? Something flexible enough to also defined the required (as optional values) and for icon the png/svg options as a single string.

Is this perhaps more than TS can handle?

Upvotes: 2

Views: 662

Answers (1)

Tobias S.
Tobias S.

Reputation: 23865

The first thing we need to do here is to use as const again to preserve the type information of items. Also items should be a tuple and not an object to make the following logic easier.

const items = [{
  name: "navigation",
  type: "list",
  subFields: [
    {
      name: "label",
      type: "string",
      defaultValue: "",
      require: true,
    },
    {
      name: "url",
      type: "url",
      defaultValue: "",
      require: true,
    },
    {
      name: "images",
      type: "list",
      subFields: [
        {
          name: "label",
          type: "string",
          defaultValue: "",
          require: true,
        },
        {
          name: "icon",
          type: "file",
          allowedFileTypes: ["png", "svg"],
        },
      ],
    },
  ],
}] as const

A possible solution for this problem would then look like this:

type ExpandRecursively<T> = T extends object
  ? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
  : T;


type GenerateItems<T extends readonly any[]> = {
  [K in keyof T & `${bigint}` as T[K] extends { name: infer N extends string } 
    ? N 
    : never
  ]: 
    T[K] extends { subFields: infer S extends readonly any[] } 
      ? GenerateItems<S>[]
      : T[K] extends { type: infer Type extends string } 
        ? Type extends "string"
          ? string 
          : Type extends "url"
            ? string
            : Type extends "file"
              ? T[K] extends { allowedFileTypes: infer FT extends readonly string[] }
                ? FT[number]
                : never
              : never
        : never
}

This solution requires a lot of manual checking of the type field. We have to manually check the field for each possible value and act accordingly.

type Items = ExpandRecursively<GenerateItems<typeof items>>

// type Items = {
//     navigation: {
//         label: string;
//         url: string;
//         images: {
//             label: string;
//             icon: "png" | "svg";
//         }[];
//     }[];
// }

Playground

Upvotes: 2

Related Questions