Home > Software design >  Match generic nested object type in Typescript
Match generic nested object type in Typescript

Time:05-03

Suppose I have a nested object type for defining, let's say, a user:

type TestUser = {
    name: string;
    email: string;
    houses: {
        address: string;
        rooms: {
            floor: number;
            color: string;
            connectedTo: {
                id: number;
            }[];
        }[];
        doors: {
            location: number;
            size: number;
        }[];
    }[];
    tasks: {
        description: string;
        people: {
            nickname: string;
        }[];
    }[];
}

I'd like to define a generic function that can take in this type, and then a specification of the nesting properties of the type; for example, in this case, this might look like:

const houseSpecification = [{
    name: 'houses',
    children: [
        // (not all subproperties of the type need to be defined, e.g. note how the 
        // 'tasks' branch is missing altogether)
        {
            name: 'rooms',
            children: [
                {
                    name: 'id',
                },
            ],
        },
        {
            name: 'doors',
        },
    ],
}],

In each branch, the 'name' property specifies the name of a property of the parent that is an object array type. The type of the object in the array is then taken as the source of 'name's for entries in the 'children' property.

I'm looking for a definition of this specification type in some function taking this format for any given object type, e.g. the type of Spec in function myFunction<T extends object>(spec: Spec<T>[]) such that myFunction<TestUser> takes houseSpecification as its argument.

I have something which I believe should work, but for some reason is seemingly not recursing properly:

// extracts the type of an array, e.g. ArrayType<({ id: string })[]> -> { id: string }
export type ArrayType<T> = T extends (infer U)[] ? U : never;

// gives a union type of all keys in an object that yield an array of objects, 
// e.g. ObjectArrKeys<{ id: string, data: object[], users: object[] }> -> 'data' | 'users'
export type ObjectArrKeys<T> = {
    [K in keyof T]: T[K] extends object[] ? K : never;
}[keyof T];

// used as `function myFunction<T extends object>(spec: Spec<T, ObjectArrKeys<T>>[])`
export type Spec<T extends object, Q extends ObjectArrKeys<T>> = {
    name: Q;
    children?: Spec<
            ArrayType<T[Q]>,
            ObjectArrKeys<ArrayType<T[Q]>>
    >[]
}

I've tested both utilities, ArrayType and ObjectArrKeys, and they seem to be working correctly. However, Typescript doesn't seem to like the recursion in Spec, complaining that ArrayType<T[Q]> doesn't match the type object, despite the fact that Q can only represent of key of T that yields an object array.

Is there a simpler way of getting to my end result? Why is Typescript not inferring these types correctly and claiming that it's not specific enough?

Thanks.

CodePudding user response:

If I understood the question correctly this should solve the problem:

type Spec<T> = {
  [key in ObjectArrKeys<T>]: {
    name: key, children?: T[key] extends (infer I)[] ? Spec<I> : never
  }
}[ObjectArrKeys<T>][]

First of all, since houseSpecification and the nested children properties are arrays, I will also let Spec return an array type. I iterate over all keys with array properties and construct a union of all possible name and children combinations. For each iteration I then take the inferred element type and recursively call Spec.

Before passing the nested object into the recursive Spec call I again check that T[key] really does extends (infer I)[]. I think this information got lost before and TypeScript didn't really know that T[key] is of type any[] even though ObjectArrKeys filtered all array keys.

Playground

  • Related