1

I would like to find a way to have all the value types for all the keys of a nested object paths.

I am successful to a certain extent but failed to set value type for a deep nested property inside an array object.

interface BoatDetails {
  boats: {
    boat1: string;
    boat2: number;
  };
  ships: Array<Ship>
}
interface Ship {
    shipName: string
}
const boatDetails: BoatDetails = {
  boats: {
    boat1: "lady blue",
    boat2: 1,
  },
  ships: [
      {shipName: "lacrose"}
  ]
};

For the above code I am able to successfully set the value type for the nested object paths like boats.boat1 whose value type is string, boats.boat2 whose value type is number, ships whose value type is Array<Ship>.

But unable to set value type for the the nested path ships.0.shipName.

I have taken reference for setting the deep nested object path types from the below link: Typescript: deep keyof of a nested object

Below is my attempt towards setting the value type for deep nested object paths in typescript playground:

Playground link for seeting value type for deep nested object paths

cvss
  • 411
  • 1
  • 4
  • 11
  • Does this answer your question? https://stackoverflow.com/questions/67606750/updating-nested-properties-from-an-object-with-bracket-notation-in-typescript – omidh May 19 '21 at 16:22
  • Sorry, it did not answer, but thanks for looking into it. – cvss May 19 '21 at 16:45
  • Well, I believe the problem is the `0` index is not enforced to present in `Array` type. If you switch `ships`'s type to `[Ship]` your code works. – aleksxor May 19 '21 at 18:58
  • Note that the `V` type parameter in your `setValue()` function doesn't really do anything; you should probably remove it and replace all references to `V` with `PathValue` directly. – jcalz May 19 '21 at 19:12

1 Answers1

4

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

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thank you for such an amazing answer. Your knowledge on how typescript compiler works is helping typescript community a lot. – cvss May 19 '21 at 22:39
  • I've tried to modify your answer so instead of just replacing the value you can provide a function that has the current value as the first parameter and return the new value but couldn't figure it out. Is it possible? – zomars Oct 18 '22 at 19:23
  • Like [this](https://tsplay.dev/NB5Qpw) maybe? I can't spend a lot of time focusing on followup questions on old q/a pairs; if you continue to have an issue you might want to open your own post to get more eyes on it. Good luck! – jcalz Oct 18 '22 at 22:43