Issue
VS Code is telling me
Type instantiation is excessively deep and possibly infinite.ts(2589)
My type is relatively simple, but is indeed recursive.
export type NestedObjectValue<
T extends Record<string, any>,
K extends Readonly<string>
> =
K extends `${infer F extends string}.${infer R extends string}`
? NestedObjectValue<T[F], R>
: K extends keyof T
? T[K]
: never
I have potentially large models (600+ records) that will look something like
type Item = {
name: string,
children: Item[]
}
I have a type that reduces a given hierarchy like a string where each level's name
is joined separated by a ".". So for example "grandparent.parent.child.grandchild".
I wrote the type NestedObjectValue
to be given one of those "dot notation strings", traverse down the "potentially large models" until it gets to the grandchild and return it's value.
Is there anything here that's clearly inefficient? Any suggestions on how I can avoid the TS error?
Solution
Since there's no minimal reproduction and I used something like this myself, and also encountered a similar error, I think the problem is with using T extends Record<string, any>
and later looking into the keys of T
using K extends keyof T
. Typescript hangs trying to process any
, so replacing it with unknown
and asserting a type later might help.
Here's an updated type — I simplified it a bit and added some endpoints — feel free to change it however you want:
export type NestedObjectValue<
T extends Record<string, unknown>,
K extends string
> =
K extends `${infer F}.${infer R}`
? (
F extends keyof T
? (
T[F] extends Record<string, unknown>
? NestedObjectValue<T[F], R>
: T[F]
)
: never
)
: (
K extends keyof T
? T[K]
: never
)
;
type Item = {
name: string,
children: Item[],
foo: {
bar: string
},
optionalProp?: string,
optionalRecord?: {
bar: string
}
}
type T1 = NestedObjectValue<Item, 'children'>;
// ^ Item[]
type T2 = NestedObjectValue<Item, 'name'>;
// ^ string
type T3 = NestedObjectValue<Item, 'foo.bar'>;
// ^ string
type T4 = NestedObjectValue<Item, '123'>;
// ^ never
type T5 = NestedObjectValue<Item, 'name.prop'>;
// ^ string
type T6 = NestedObjectValue<Item, 'optionalProp'>;
// ^ string | undefined
type T7 = NestedObjectValue<Item, 'optionalRecord.bar'>;
// ^ { bar: string; } | undefined
// ^^ since this might be undefined, the traversel doesn't go deeper into the record
There's an issue tho with the optional types. If a type is an optional record (object), the traversal will not go into that type since this will not be true: T[F] extends Record<string, unknown>
. I've solved it my making all properties required, but that might not be a solution for you. Anyway, here's the type I use with that solution:
/**
* Lookups the value type of a property at given arbitrary key path.
*
* If a path doesn't exist, results in `undefined`.
* If a non-record property type is encountered along the path, results in `never`.
*/
export type KeyPathValueAtAnyPath<T extends Record<string, unknown>, Path extends string> =
// check if we extend the key path pattern
Path extends `${infer L}.${infer R}`
? (
// if yes, check if the first part of the path extends the keys of current record
// note: the use of `Required` is due to the optional prop paths resulting in never otherwise
L extends keyof Required<T>
? (
// if yes, check if the value of the target prop is a record
Required<T>[L] extends Record<string, unknown>
// if yes, continue the lookup
? KeyPathValueAtAnyPath<Required<T>[L], R>
// if no, we're stuck due to the path not being over yet.
// the end value cannot be counted as undefined, since one of its parent props is of a non-record type,
// so the path is not reachable whatsoever, hence the never
: never
)
// if the first part of the path doesn't extend the keys of the current record keys,
// then we're stuck due to the path not being over yet.
// if no key extend the path, its doesnt exist, hence the undefined
: undefined
)
// if we dont' extend the pattern, than we either:
// - reached the end of it
// - encountered not a valid pattern
// there's not way to know, so lets just hope and pray, and try to to retrieve the value (the type)
: (
// if there a key
Path extends keyof T
// there a way — return the value (type) of that key (value)
? T[Path]
// if no key extend the path, its doesnt exist, hence the undefined
: undefined
)
I also have a type that generates these paths and another that gets the value type by using one of these paths with autosuggest on the path. And also the tests for all these types. And also - a few functions that utilize these "paths" (property value getter/setter, property existance), and tests for them too. Feel free to comment if you need those, I'm more than happy to share :)
Answered By - Murolem
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.