Home > Enterprise >  Recursive function type based on object keys
Recursive function type based on object keys

Time:01-23

Im trying to create a recursive function type that takes the outer most keys in an object type as an argument and returns a new function that then takes the next outer most keys of the object from the given key recursively until no more keys are available.

If the key doesn't extend /${string} the argument must be of an object type containing the key ans and property:

type Argument<T> = T extends `/${string}` ? T : { [key in T]: string | number }

There will only be one key that wont extend /${string} per. level.

Example

const values = {
    '/aaa': {
        '/lorem': {
            foo: 'hello world',
            '/boo': 12345,
        },
    },

    bbb: {
        '/ipsum': {
            '/dolor': 'lorem ipsum',
            amet: 567890,
        },
    },

    '/ccc': ...
};

foo<typeof values>('/aaa')('/lorem')({ foo: '...' }); // should return type of string
foo<typeof values>('/aaa')('/lorem')('/boo'); // should return type of number
foo<typeof values>('/aaa')('/ipsum'); // should fail

foo<typeof values>({ bbb: '...' })('/ipsum')({ amet: '...' }); // should return type of number
foo<typeof values>({ bbb: '...' })('/ipsum')('/dolor'); // should return type of string
foo<typeof values>({ bbb: '...' })('/lorem'); // should fail

My current code

I have a type that almost does the job., but doesn't work with the non /${string} extensions :(

type Foo<T extends object> = <K extends keyof T>(args: K) => T[K] extends object ? Foo<T[K]> : T[K];

const values = {
    '/aaa': {
        '/lorem': {
            foo: 'hello world',
            '/boo': 12345,
        },
    },

    bbb: {
        '/ipsum': {
            '/dolor': 'lorem ipsum',
            amet: 567890,
        },
    },
};


const foo = {} as Foo<typeof values>

foo('/aaa')('/lorem')('/boo') // works :D
foo('/aaa')('/lorem')({ foo: '...' }) // fails :(

I tried to handle the args type within the function - But it can't return the next keys :(

type Foo<T extends object> = <K extends keyof T>(
    args: K extends `/${string}` ? K : { [key in K]: string | number }
) => T[K] extends object ? Foo<T[K]> : T[K];

const values = {
    '/aaa': {
        '/lorem': {
            foo: 'hello world',
            '/boo': 12345,
        },
    },

    bbb: {
        '/ipsum': {
            '/dolor': 'lorem ipsum',
            amet: 567890,
        },
    },
};


const foo = {} as Foo<typeof values>

foo('/aaa')('/lorem')('/boo') // works :D
const a = foo({ bbb: '...' })('/ipsum') // fails :(

I don't even know if want I'm trying is possible - but if you have any suggestions you would save my day :)
Thanks for your time.

CodePudding user response:

Hmm, problem seems to be with {[key in K]: string | number}, apparently typescript does not know if T[K] is an object after that. If you change it to {[key: string]: K} it works (but {[key: string|number]: K} does not). This seems like an issue with Typescript (I am using version 4.7.4.)

Best I can do is using a second parameter instead of an object:

type Foo<T extends object> = <K extends keyof T>(
  arg: K,
  val?: K extends `/${string}` ? undefined : string|number
) => T[K] extends object ? Foo<T[K]> : T[K];


const foo = {} as Foo<typeof values>

const boo = foo('/aaa')('/lorem')('/boo') // TS: boo is number
const ipsum = foo('bbb', 12)('/ipsum') // TS: ipsum is Foo<{'/dolor': string, amet: number}>
  • Related