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');