9

Is it possible to type an array of strings in such a way that the array can only be a valid property path in a given object? The type definition should work for all deeply nested objects.

Example:

const object1 = {
    someProperty: true
};
const object2 = {
    nestedObject: object1,
    anotherProperty: 2
};

type PropertyPath<Type extends object> = [keyof Type, ...Array<string>]; // <-- this needs to be improved

// ----------------------------------------------------------------

let propertyPath1: PropertyPath<typeof object1>;

propertyPath1 = ["someProperty"]; // works
propertyPath1 = ["doesntExist"]; // should not work

let propertyPath2: PropertyPath<typeof object2>;

propertyPath2 = ["nestedObject", "someProperty"]; // works
propertyPath2 = ["nestedObject", "doesntExist"]; // should not work
propertyPath2 = ["doesntExist"]; // should not work

Link to TypeScript playground

Johannes Ewald
  • 17,665
  • 5
  • 44
  • 39
  • I don't really understand the "should work for all deeply nested objects.", because that seems impossible. You can however make the type: `type PropertyPath = (keyof Type)[];` – Poul Kruijt Dec 23 '19 at 13:16

2 Answers2

13

In the answer to the question this duplicates you can use the recursive Paths<> or Leaves<> type aliases, depending on whether or not you want to support all paths that start at the root and end anywhere in the tree (Paths<>) or if you want to only support paths that start at the root and end at the leaves of the tree (Leaves<>):

type AllPathsObject2 = Paths<typeof object2>;
// type AllPathsObject2 = ["nestedObject"] | ["nestedObject", "someProperty"] | 
//  ["anotherProperty"]

type LeavesObject2 = Leaves<typeof object2>;
// type LeavesObject2 = ["nestedObject", "someProperty"] | ["anotherProperty"]

I'll assume it's Paths but you can change it to Leaves if that fits your use case. Here's the behavior you get, which matches what you asked for:

let propertyPath1: Paths<typeof object1>;
propertyPath1 = ["someProperty"]; // works
propertyPath1 = ["doesntExist"]; // error!
//               ~~~~~~~~~~~~~~

let propertyPath2: Paths<typeof object2>;
propertyPath2 = ["nestedObject", "someProperty"]; // works
propertyPath2 = ["nestedObject", "doesntExist"]; // error!
//                               ~~~~~~~~~~~~~
propertyPath2 = ["doesntExist"]; // error!
//               ~~~~~~~~~~~~~

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Awesome, thank you! `Paths` is what I was looking for. This is this kind of type you never want to have in your codebase and you're happy someone else did it for you :D – Johannes Ewald Dec 23 '19 at 17:34
  • This is probably the most serious Typescript voodoo I've ever seen. Nice! – staeke Jul 10 '20 at 04:39
  • Is there any better solution to access the keys using the same data which is in the array without joining them – Shivam Sep 28 '21 at 10:18
  • @Shivam I don't understand the question, sorry. – jcalz Sep 28 '21 at 15:38
  • @jcalz what I meant was instead of accessing or defining them within arrays ["someProp"] then accessing the object key value with array[0], how can we just use the path as string eg "someProp" ? – Shivam Sep 30 '21 at 11:09
  • Are you asking for a special case for tuples of length one? If not, what do you want to see for nested properties? Also note that comments on year-old questions aren’t necessarily the best place to get followup answers. – jcalz Sep 30 '21 at 13:07
  • This is great, thank you! Is there a way to get the type of the value too? E.g. ```function setByPath(obj: Obj, path: Key, value: ???) { }``` – filipovskii_off Mar 20 '23 at 10:45
  • UPD to my prev comment: I think I found an answer, link: https://codesandbox.io/s/typescript-playground-export-forked-sznv9i – filipovskii_off Mar 20 '23 at 14:59
-5

It's possible using arrow functions

const object1 = {
    someProperty: true
};
const object2 = {
    nestedObject: object1,
    anotherProperty: 2
};

type PropertyPath<Type extends object> = (x: Type) => any;

let propertyPath1: PropertyPath<typeof object1>;

propertyPath1 = (x) => x.someProperty; // works
propertyPath1 = (x) => x.doesntExist; // should not work

let propertyPath2: PropertyPath<typeof object2>;

propertyPath2 = (x) => x.nestedObject.someProperty; // works
propertyPath2 = (x) => x.nestedObject.doesntExist; // should not work
propertyPath2 = (x) => x.doesntExist; // should not work

Playground Link

Damir Shakenov
  • 331
  • 4
  • 17