1

Is it possible to Pick nested object elements via dot notation?

interface Test {
   customer: {
      email: string;
      name: {
         firstName: string;
      };
   };
};

type PickedTest = PickByDotNotation<Test, "customer.name.firstName">;

PickedTest has type equal to { customer: { name: { firstName: string } } };

My need is to have generic supporting multiple paths, as below.

type PickedTest = PickByDotNotation<Test, "customer.name.firstName" | "customer.email>;
czlowiek488
  • 187
  • 2
  • 17

2 Answers2

2

Looks like a nice code interview question ;) This is possible using template literal types introduced already quite a while ago. Here is how:

type PickByDotNotation<TObject, TPath extends string> =
  // If TKey has a dot, split it into two: the part before the dot and after the dot
  TPath extends `${infer TKey}.${infer TRest}` ?
    // Checking if the key actually exists in the object
    TKey extends keyof TObject ?
      // Get type recursively
      PickByDotNotation<TObject[TKey], TRest> :
      // Provided key is invalid
      never :
  // The path doesn't contain a dot, so just trying to use it as a key
  TPath extends keyof TObject ?
    TObject[TPath] :
    never

Playground link

You can make the first clause a bit simpler using infer ... extends ... introduced in TS 4.7

type PickByDotNotation<TObject, TPath extends string> = 
    // Constraining TKey so we don't need to check if its keyof TObject
    TPath extends `${infer TKey extends keyof TObject & string}.${infer TRest}` ?
        PickByDotNotation<TObject[TKey], TRest> :
    TPath extends keyof TObject ?
        TObject[TPath] :
        never

Playground link

However I'd not recommend to use this type in actual code, at least I cannot guarantee that it will always work as you want it to. Particularly the template string part: I'm pretty sure if there are multiple dots in TPath TS documentation never says if TKey will be the part before the first dot or before the last one, or maybe even some random dot in the middle

Alex Chashin
  • 3,129
  • 1
  • 13
  • 35
  • In my case I generate dot notation access paths on my own. I have type `A` and it is a complex nested object with types from external libraries. I create a dot notation from `A` and then I allow to `Pick` - literally lodash `pick` function used on object with `A` type. What do you think? Is your solution suitable? – czlowiek488 Jul 04 '22 at 21:48
  • Well, it kind of is, but the point remains: I don't think typescript guarantees that it will always pick the first point when resolving `${infer TKey}.${infer TRest}`. So you can use it and I think it will work, but keep in mind that it may break and no one holds responsibility for that – Alex Chashin Jul 07 '22 at 19:00
  • This isn't `Pick`ing, it's indexing. (e.g., `Pick<{a: string, b: number}, "a">` is `{a: string}`, not `string`). I see this answer was accepted, but maybe the question should be changed to ask about indexing and not picking? – jcalz May 13 '23 at 20:48
1

The question is asking about Pick (where the result is the supertype of the object containing just the picked properties) and not indexing (where the result is the value types of the properties at the path), so I'm going to leave an answer here that actually does the Pick:

type PickByDotNotation<T, K extends string> = {
    [P in keyof T as P extends (K extends `${infer K0}.${string}` ? K0 : K) ? P : never]:
    P extends K ? T[P] : 
      PickByDotNotation<T[P], K extends `${Exclude<P, symbol>}.${infer R}` ? R : never>
} & {} 

Let's test it out on your examples:

interface Test {
    customer: {
        email: string;
        name: {
            firstName: string;
        };
    };
    anotherProp: number;
};

type PickedTest = PickByDotNotation<Test, "customer.name.firstName">;
/* type PickedTest = {
    customer: {
        name: {
            firstName: string;
        };
    };
} */

type PickedTest2 = PickByDotNotation<Test, "customer.name.firstName" | "customer.email">;
/* type PickedTest2 = {
    customer: {
        email: string;
        name: {
            firstName: string;
        };
    };
} */

Looks good!

Playground link to code

Note: I could come back here and elaborate on how it works if anyone cases, but right now I just want to make sure there's an answer here to the question as asked.

jcalz
  • 264,269
  • 27
  • 359
  • 360