Recursive nesting typings with selected properties

91 views Asked by At

So I have the following interfaces & types:

interface A {
    A(): void;
}

interface B {
    B(): void;
}

interface C {
    C(): void;
}

type All = A & B & C;

and i want to extract a partial set of the methods AND create another nesting level which can do the same thing recursively: [typescript version 4.1.0]

type SelectMethods<
    METHODS extends Partial<All>, 
    MORE extends Record<string, SelectMethods<any>> = {}
> = Pick<All, Extract<keyof All, keyof METHODS>> & MORE;
  • Taking a partial set of All
  • Picking only the relevant selected methods
  • Merging it with a Record that for each property do the same

this allows me to do the following, which works great:

const obj: SelectMethods<A & B, { x: SelectMethods<A & B & C> }> = {
    A() {
    },
    B() {
    },
    x: {
        A() {
        },
        B() {
        },
        C() {
        }
    }
};

But when removing the C (or any of the others) in the inner SelectMethods it stop working: [error appears for the type literal between the stars]

const obj: SelectMethods<A & B, **{ x: SelectMethods<A & B> }**> = {  // Property 'C' is missing in type 'Pick<All, "A" | "B">' but required in type 'Pick<All, "A" | "B" | "C">'.
    A() {
    },
    B() {
    },
    x: {
        A() {
        },
        B() {
        }
    }
};

things i've tries so far:

  • conditional types to try proving to the compiler it's ok:
type SelectMethods<METHODS extends Partial<All>, MORE extends Record<string, SelectMethods<any>> = {}> =
    Pick<All, Extract<keyof All, keyof METHODS>> & (
        MORE extends Record<string, infer DEEP> ?
            DEEP extends SelectMethods<infer SUBMETHODS> ? 
                SUBMETHODS extends Partial<All> ?
                    MORE
                    : never
                : never
            : never
    );

but i'm getting TS2589: Type instantiation is excessively deep and possibly infinite.

any ideas? :)

1

There are 1 answers

0
Linda Paiste On BEST ANSWER

The problem is with

Pick<All, Extract<keyof All, keyof METHODS>>

Let's make that it's own type so that we can play with it:

type MethodPick<METHODS extends Partial<All>> = Pick<All, Extract<keyof All, keyof METHODS>>

This works fine on the first level. type AB = MethodPick<A & B> is just A & B.

But what is the actual type of SelectMethods<any> aka SelectMethods<Partial<All>>? You might think that it would be a partial selection of methods, but it's not. It actually requires that A, B, and C are all set. This is because when we do our MethodPick, we take the keys of what we are picking and get those methods. But the flaw is we haven't checked if those keys are required. keyof Partial<All> is 'A' | 'B' | 'C', so type P = MethodPick<Partial<All>> is A & B & C. That means SelectMethods<any> is A & B & C where all three methods are required.

MORE extends Record<string, SelectMethods<any>>

So the above statement means that the values of this record must extend All, not Partial<All>. And that's not what we want.

If we already know that METHODS extends Partial<All>, why do we need to Pick at all?

We simplify the type to just = METHODS & MORE and we replace SelectMethods<any> with SelectMethods<Partial<All>> since any isn't really a valid input and we get this:

type SelectMethods<
    METHODS extends Partial<All>, 
    MORE extends Record<string, SelectMethods<Partial<All>>> = {}
> = METHODS & MORE;

This should resolve your issues.

Typescript Playground Link