Reputation: 687
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
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";
// }[];
// }[];
// }
Upvotes: 2