I'm trying to make a helper function that takes a nested object like JSON, and allows one to make a deep copy of a nested value at any arbitrary depth. I understand variadic tuple types and can get them to work for just passing around the tuples - but I don't know how to 'map' them to nested Picks of arbitrary depth (it may not even be possible). Here's the best I've come up with - but is still limited to needing to create as many overloads for GetNestedValue as I would care to support. I understand the various errors, I just can't come up with any way to satisfy the compiler and get type completion on the return value.
// K is arbitrary length how to express N accessors deep? in TS without a loop?
type GetNestedValue<K extends string[], O extends any> = O[K[0]][K[1]][K[2]];
function getNestedItem<Keys extends string[], Obj>(
obj: Obj, ...keys: readonly [...Keys]
): GetNestedValue<Keys, Obj> extends undefined ? undefined : GetNestedValue<Keys, Obj> {
let level: any = obj;
for (const key of keys) {
if (level !== undefined) {
level = level[key];
} else {
return;
}
}
// this will return deepClone(level);
return level;
}
const obj = {one: 1, two: {three: {four: 4}}};
// I'd prefer 'known' shapes of obj here block form entering invalid keys.
const a = getNestedItem(obj, 'one', 'two');
// here - when arbitrarily trying to grab stuff from unknown inputs - I don't want
// a warning, rather the user would just need to check `if (b !== undefined)`
const b = getNestedItem(obj as any, 'one', 'two');
link to playground
I'll lead off by saying: While a fun thought experiment, I wouldn't recommend this due to the amount of recursion it requires.
It requires two recursive types, A type to get a valid set of keys inferred from an object type, and a getter to access the property given those validated keys. For TypeScript < 4.5, the depth limit will be a tuple of length 10.
Validation:
Getter:
In order for a function to correctly infer a generic, that generic needs to be present as a possible argument. What we want is
ValidKeys
, but we can't do that without havingKeys
itself as a potential argument. So we make use of a conditional for the...keys
argument to force it to resolve.Regarding the return type, Even though
GetNestedProp
will potentially be a union withundefined
, the compiler can't infer that it definitely is in the case where your else branch is hit. So you can either make the return type this awkward conditional, or//@ts-expect-error
the else branch return statement with a simpler return type ofGetNestedProp<Keys, Obj>
. That alternative is included in the playground:Given a type with an optional property, digging into that property will convert the nested property type into a union with undefined:
playground