Access specific properties of an object in TypeScript using dot notation with the

Can we use dot notation to extract nested object elements in TypeScript?

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

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

PickedTest is of the type

{ customer: { name: { firstName: string } } };

I am looking for a generic solution that can handle multiple paths, like the example below.

type PickedTest = ExtractNested<Test, "customer.name.firstName" | "customer.email">;

Answer №1

This looks like a great question for a coding interview ;) You can achieve this using template literal types that were introduced quite a while ago. Here's how:

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

Playground link

You can simplify the first clause using infer ... extends ... from TS 4.7

type ExtractByDotNotation<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}` ?
        ExtractByDotNotation<TObject[TKey], TRest> :
    TPath extends keyof TObject ?
        TObject[TPath] :
        never

Playground link

However, I wouldn't recommend using this type in production code, as there may be uncertainties in how the template string part behaves with multiple dots in TPath. TS documentation doesn't specify if TKey will be before the first dot, last dot, or somewhere random in between.

Answer №2

The query pertains to the concept of Pick (where the output is the higher-level type of the object containing specific chosen properties) rather than indexing (which returns the value types of properties at a given path). I will provide a solution here focusing on 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 see how it performs 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;
        };
    };
} */

It appears to be functioning correctly!

Access the code in TypeScript Playground

Note: I can provide further explanation on how this solution operates if needed, but for now, I wish to ensure a response is available to the original question.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Utilizing the 'create' function in sqlite each time I need to run a query

I've been diving into SQLite within the Ionic framework and have pieced together some code based on examples I've encountered. import { Component } from '@angular/core'; import { IonicPage, NavController, NavParams } from 'ionic-a ...

experimenting with asynchronous promises in Jasmine to test Angular 2 services

Currently, I'm facing some challenges while testing my Angular 2 service. Even though my tests are passing, I keep encountering this error in the console: context.js:243 Unhandled Promise rejection: 'expect' was used when there was no c ...

What is the best way to initiate a class constructor with certain parameters left out?

Currently, I am facing a challenge while trying to pass various combinations of arguments to a class constructor. The constructor has 2 optional arguments. Here is the code snippet: class MyClass { foo(a, b) { return new MyClass(a, b); } bar( ...

Ways to trigger a function in Angular every 10 seconds

What is the method to utilize Observable function for fetching data from server every 10 seconds? Custom App service fetchDevices (): Observable<Device[]> { return this.http.get(this.deviceUrl) .map(this.extractData) .catch(this ...

When the down arrow key is pressed, the focus of the radio button is lost

I've been facing a persistent accessibility issue with the main-component in Angular. This component contains four different templates, but depending on the radio button selection, other templates are displayed. The problem arises when these templates ...

JavaScript: Navigating function passing between multiple React components

I'm currently working on a React Native application utilizing TypeScript. In my project, there is a component named EmotionsRater that can accept two types: either Emotion or Need. It should also be able to receive a function of type rateNeed or rate ...

Adjusting canvas height in Storybook - Component does not fit properly due to low canvas height

I had a component that I needed to add to Storybook. It was working fine, but the styling was slightly off. I managed to resolve this by adding inline styling with position: absolute. Here is how it looks now: const Template: any = (args: any): any => ( ...

Typescript: Declaring object properties with interfaces

Looking for a solution to create the childTitle property in fooDetail interface by combining two properties from fooParent interface. export interface fooParent { appId: string, appName: string } export interface fooDetail { childTitle: fooParent. ...

Issues with Vite's global import feature not functioning properly in a production build

My current setup involves loading all markdown files within a directory using a glob import. The code snippet below depicts this functionality: const useGetChangelogs = () => { const [changelogs, setChangelogs] = useState<string[]>([]); useEf ...

Step-by-step guide on setting up pnpm directly, without the need to first

Here I am, faced with a brand new Windows 10 installation - only VS Code is installed and nothing more: Can pnpm be installed and used without the need for npm? Is this action beneficial or detrimental? Consideration: typescript ...

Examining Resolver Functionality within NestJS

Today I am diving into the world of writing tests for NestJs resolvers. I have already written tests for my services, but now it's time to tackle testing resolvers. However, I realized that there is a lack of documentation on testing resolvers in the ...

Customize the border color of a dynamic textbox with Angular

I'm using Angular to create dynamic textboxes. <span *ngFor="let list of lists[0].question; let i = index"> {{ list }} <input type="text" *ngIf="i != lists[0].question.length-1" [(ngModel)] ...

Running JavaScript code when the route changes in Angular 6

Currently, I am in the process of upgrading a website that was originally developed using vanilla JS and JQuery to a new UI built with Angular and typescript. Our site also utilizes piwik for monitoring client activity, and the piwik module was created i ...

Using NgModel with a custom element

I am currently using a basic component within my form as shown below: <app-slider [min]="field.min" [max]="field.max" [value]="field.min"></app-slider> This component consists of the following code: HTML: <input #mySlider class="s ...

Every time I attempt to destructure the state object in react typescript, I encounter the error message stating 'Object is possibly undefined'

Whenever I attempt to destructure my state object in react typescript, I encounter an error stating Object is possibly 'undefined'. When I try using optional chaining, a different error pops up saying const newUser: NewUser | undefined Argument o ...

Showing elapsed time similar to YouTube in an Angular 8 application

Currently, I am developing an Angular application to replicate certain features found on YouTube by utilizing data fetched from an API. This API provides video timestamps in a string format Each timestamp follows this structure : YYYY-MM-DDTHH:MM:SS For ...

Issue with MUI icon import: React, Typescript, and MUI type error - Overload does not match this call

Within my component, I am importing the following: import LogoutIcon from "@mui/icons-material/Logout"; import useLogout from "@/hooks/auth/useLogout"; const { trigger: logoutTrigger } = useLogout(); However, when utilizing this compo ...

How come TypeScript remains silent when it comes to interface violations caused by Object.create?

type Foo = { x: number; }; function g(): Foo { return {}; // Fails type-check // Property 'x' is missing in type '{}' but required in type 'Foo'. } function f(): Foo { return Object.create({}); // Passes! } functio ...

Typescript issue: variable remains undefined even after being assigned a value in the constructor

In my code, I declared num1: number. constructor(){ ... this.num1 = 0; } This is all inside a class. However, when I try to log the value of this.num1 or typeof this.num1 inside a function using console.log(), it returns undefined in both cases. I ...

Troubleshooting: Vue and TypeScript Components Not Communicating

Vue is still fairly new to me and I'm struggling with this issue. Despite following code examples, my implementation doesn't seem to be working correctly. The component I defined looks like this: <template> <div class="row"> ...