3

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>;
1

2 Answers 2

4

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

Sign up to request clarification or add additional context in comments.

3 Comments

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?
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
This isn't Picking, 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?
3

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.