Home > front end >  Recursive types based on object keys
Recursive types based on object keys

Time:01-05

Im trying to create a function 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.

Example

const values = {
  a: {
    aa: {
      aaa: 'hello aaa',
    }
  },
  b: {
    bb: 'hello bb',
  }
} as const;

// should work
foo<typeof values>('a')('aa')('aaa');
foo<typeof values>('b')('bb');

// should fail
foo<typeof values>('c');
foo<typeof values>('a')('b');

I already made a function that does something like this but fails whenever there is more than one key at the same level :(

My current code

const valuesA = {
  a: {
    aa: {
      aaa: 'hello aaa',
    }
  }
} as const;

const valuesB = {
  a: {
    aa: {
      aaa: 'hello aaa',
    }
  },
  b: {
    bb: {
      bbb: 'hello bbb',
    }
  }
} as const;


function foo<T>() {
  return function (key: keyof T) {
    type U = T[typeof key];
    return foo<U>();
  };
}

const booA = foo<typeof valuesA>();
const booB = foo<typeof valuesB>();

booA('a')('aa')('aaa'); // WORKS! :D
booB('a')('aa');        // FAILS! :(

booB give the following error message:

Argument of type 'string' is not assignable to parameter of type 'never'.

Is there a way to make B work? - Cheers

CodePudding user response:

Checkout this recursive conditional type:

type ValueOrFn<T> = <K extends keyof T>(key: K) =>
  T[K] extends object
    ? ValueOrFn<T[K]>
    : T[K]

This type takes T as a parameter and returns a function type that accepts a keyof T.

The return value of that function is either T[K] we found a primitive value, or recursively return ValueOrFn<T[K]> if we have an object and therefore expect further drill ins.

Let's try it:

declare const foo: ValueOrFn<typeof values>

const test1A = foo('a')('aa')('aaa') // 'hello aaa'
const test1B = foo('b')('bb') // 'hello bb'

One thing to note here is that this can't work:

foo<typeof values>('a');

This is because the invocation of foo requires two generic type parameters:

  • typeof values The object to drill into.
  • 'a' the key to use to drill into the object.

You need both of those in order for the type system to do T[K] and drill into that object.

But here one is explicit (the typeof values) and one is inferred (the key 'a'). And in typescript all generic function parameters must be either explicit or inferred.


But there are some work arounds:

So you could wrap it in a function, which separates this into two function calls.

declare const fooWrapped: <T>() => ValueOrFn<T>

const test2A = fooWrapped<typeof values>()('a')('aa')('aaa');
const test2B = fooWrapped<typeof values>()('b')('bb');
//                                      ^ Note extra parens

However, this is only a problem if you don't accept value that you can get the type from. And by doing so, it serves as a natural way to break of the type parameters anyway.

declare const fooObj: <T>(obj: T) => ValueOrFn<T>

const test3A = fooObj(values)('a')('aa')('aaa');
const test3B = fooObj(values)('b')('bb');

See Playground

  • Related