Caution: the usage of recursive conditional types combined with manipulation of template literal type within Paths<T>
and PathValue<T, P>
can strain the compiler (leading to potential recursion limit warnings or extensive compile times) and involves various edge cases.
One particular challenge arises when attempting to convert from number
to string
through literal types using template literals as there is no straightforward way to inversely transform string
literals back into corresponding number
literals (refer to this question and answer for more insight).
For instance, trying to use an index type like "0"
as a key in an array type will result in an error unless that array type is a tuple:
type Oops = (string[])["0"] // error!
// ------------------> ~~~
// Property '0' does not exist on type 'string[]'
type Okay = (string[])[0] // okay
// type Okay = string
This limitation causes issues when using expressions like "ships.0.shipName"
because expectedly, "0"
fails to be recognized as a valid key in an array. This lack of support leads to frustration as there is no direct method to coerce "0"
into 0
or have "0"
interpreted as keyof Ship[]
.
To circumvent this obstacle, several workarounds exist. One approach is to overlook tuples (which usually have explicit numeric-string indices except for tuple types containing rest elements) and create a workaround utilizing T[K]
checking for a number
index signature where K
is compatible with `${number}`
, returning T[number]
if so:
type Idx<T, K> = K extends keyof T ? T[K] :
number extends keyof T ? K extends `${number}` ? T[number] : never : never;
This solution proves effective:
type TryThis = Idx<string[], "0">
// type TryThis = string
type StillWorks = Idx<string[], 0>
// type StillWorks = string
By incorporating this in your PathValue<T, P>
type as shown below:
type PathValue<T, P extends Paths<T, 4>> = P extends `${infer Key}.${infer Rest}`
? Rest extends Paths<Idx<T, Key>, 4>
? PathValue<Idx<T, Key>, Rest>
: never
: Idx<T, P>
The functionality improves significantly:
setValue(
boatDetails,
`ships.0.shipName`,
"titanic"
); // okay
/* function setValue<BoatDetails, "ships.0.shipName">(
obj: BoatDetails, path: "ships.0.shipName", value: string
): BoatDetails */
While other potential workarounds may offer more precise outcomes for arbitrary pairs of T
and K
, the current implementation suffices for the time being.
Explore the code on the TypeScript Playground