Be warned: the sort of recursive conditional types with template literal type manipulation being done in Paths<T>
and PathValue<T, P>
are taxing on the compiler (you can easily get explicit recursion limit warnings, or worse, exponentially long compile times) and have various edge cases.
One edge case is that the number
-to-string
literal type conversion done so easily with template literals has no easy inverse to turn string
literals to equivalent number
literals (see this question and answer for more information).
So you are given an index type like "0"
that you want to use as the key of an array type, but unless that array type happens to be a tuple, the compiler will not let you do it:
type Oops = (string[])["0"] // error!
// ------------------> ~~~
// Property '0' does not exist on type 'string[]'
type Okay = (string[])[0] // okay
// type Okay = string
And because "0"
is not seen as a key of an array, "ships.0.shipName"
fails when your PathValue
type function evaluates "0" extends keyof Ship[]
, and you are sad. And without an official way to turn "0"
into 0
or to have the compiler see "0"
as keyof Ship[]
, there's no canonical solution.
So you're kind of stuck with various workarounds. One could be to ignore the possibility of tuples (which mostly already have explicit numeric-string indices except for those pesky rest elements in tuple types) and just make a workaround to T[K]
that checks if T
has a number
index signature and that K
is assignable to `${number}`
, and if so, returns T[number]
:
type Idx<T, K> = K extends keyof T ? T[K] :
number extends keyof T ? K extends `${number}` ? T[number] : never : never;
which now works:
type TryThis = Idx<string[], "0">
// type TryThis = string
type StillWorks = Idx<string[], 0>
// type StillWorks = string
If we use that in your PathValue<T, P>
type, like this:
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>
Then things start working:
setValue(
boatDetails,
`ships.0.shipName`,
"titanic"
); // okay
/* function setValue<BoatDetails, "ships.0.shipName">(
obj: BoatDetails, path: "ships.0.shipName", value: string
): BoatDetails */
There are other possible workarounds which might be able to tease more accurate results out of more arbitrary pairs of T
and K
, but I think this is good enough for now.
Playground link to code