2

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?

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

1 Comment

Oh wow, I think I was missing const this entire time and it was throwing me off! Your solution is a lot tighter, I'm going to experiment with it and get back to you. Thank you so much @Tobias S. you're extremely helpful and I'm learning so much from your support rn.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.