Open
Description
TypeScript Version: 4.2.0-dev.20201103
Search Terms: Recursive Conditional types, generics, tuple types
Code
// A test object
const obj = {
a: '2',
b: {
c: 3,
d: {
e: 'string',
f: {
g: {
h: 4
}
}
}
}
} as const
// its type
type Struct = typeof obj;
/**
* First recursive conditional type
*
* CheckArguments gets
* Obj - A nested object
* Arugments - A tuple of keys to go down a nested path
*
* The type is recursive, I check if the current argument lists extends keyof Obj --> Then the recursion ends
* Otherwise, I check if the first value in the tuple is keyof Obj, infer the rest, and go down the same type
* again
*/
type CheckArguments<Obj, Arguments> =
Arguments extends [keyof Obj] ? Obj[Arguments[number]] :
Arguments extends [keyof Obj, ...infer U] ? CheckArguments<Exclude<Obj[keyof Obj], string | number>, U> : never;
/**
* Tests, all 👍
*/
type Foo = CheckArguments<Struct, ['a']> // "2"
type Foo2 = CheckArguments<Struct, ['b', 'd', 'e']> // "string"
type Foo3 = CheckArguments<Struct, ['b', 'd', 'f', 'g', 'h']> // 4
/**
* Second recursive conditional type
*
* Arguments gets
* Obj - A nested Obj
*
* The recursive conditional type creates a union type of possible nested arguments in a tuple
* This is based on Anders example from TSConf: https://github.com/ahejlsberg/tsconf2020-demos/blob/master/template/main.ts
* (Dotted paths)
*/
type Arguments<Obj> =
Obj extends object ?
[keyof Obj] | SubArguments<Obj, keyof Obj> :
never;
// A helper type
type SubArguments<Obj, Key> = Key extends keyof Obj ? [Key, ...Arguments<Obj[Key]>] : never;
// For example, the possible tuples of Struct 👍
type Bar = Arguments<Struct>;
// equals to this union type
type Bar2 = ["a" | "b"] | ["b", "c" | "d"] | ["b", "d", "e" | "f"] | ["b", "d", "f", "g"] | ["b", "d", "f", "g", "h"]
/**
* So both recursive conditional types work on their own. A problem is once I want to combine
* them in a function, where I expect the first argument to bind to a value type within the Arguments union
*
* Instead of having just one value type passed to CheckArguments (the one that is bound through the generic Keys),
* TypeScript passes all parts of the union to CheckArguments. This leads CheckArguments to return all possible values
*/
declare function get<Obj extends object, Keys extends Arguments<Obj>>(o: Obj, ...keys: Keys): CheckArguments<Obj, Keys>
/**
* Tests 💥
* */
const foo = get(obj, 'a') // Should be "2" 😢
const foo1 = get(obj, 'b', 'c') // Should be 3 😢
const foo2 = get(obj, 'b', 'd', 'e') // Should be "string" 😢
const foo3 = get(obj, 'b', 'd', 'f', 'g', 'h') // Should be 4 😢
Expected behavior: Keys
gets bound to the value type passed as an argument to the function. This value type is then used for CheckArguments
Actual behavior: Keys
is the entire union type Arguments<Obj>
, not the subset. This leads to CheckArguments
returning a too broad return type (and taking very long to evaluate ;-))
Playground Link: Click here
Related Issues: Did not find any.