Determine the data type of the second element in a tuple by referencing the first element using dot notation

Background

In my current project, I'm attempting to create a secure array of path segments for navigating through an object. The interface I'm developing is specifically designed to handle objects with only two levels of depth. Eventually, these segments will be used to access properties within an object using dot notation. At this stage, my main focus is on ensuring that only valid paths can be added by enforcing strict type constraints.

Illustration

interface Days {
    monday: string;
    tueday: string;
    wednesday: string;
    thursday: string;
    friday: string;
    saturday: string;
    sunday: string;
}

interface Weekend {
    saturday: string;
    sunday: string;
}

interface Example {
    days: Days;
    weekend: Weekend;
    year: string;
}

type KeysOfUnions<T> = T extends T ? keyof T : never;
type ExamplePath<T extends keyof Example = keyof Example> = [T, KeysOfUnions<Example[T]>?];

const correctlyErrors: ExamplePath = ["days", "test"]; // this correctly triggers an error for invalid paths
const allowsCorrectPath: ExamplePath = ["days", "monday"]; // this accepts valid paths
const allowsIncorrectPaths: ExamplePath = ["weekend", "monday"]; // this should not be allowed as it is an invalid path

My current type definitions are not strict enough, as they permit any combination of path segments, including those that are impossible (such as

["weekend", "monday"]
). I've experimented with using a generic type variable with tuple types, where the first path segment serves as the type T for indexing into the Example type before retrieving its keys.

This indexing approach results in a union type of:

(Days | Weekend | string)

When applying keyof to this union type, the following error arises:

Type 'string' is not assignable to type 'never'.ts(2322)

Therefore, I resorted to using a conditional type KeysOfUnions to extract the keys of each union member, leading to overly lenient typing.

Query

How can I automatically deduce the second element (path segment) of the tuple based on the first element, and ensure that only valid combinations of path segments can be included in the array?

Edit 1: I also require a solution that accommodates single segments when there are no additional properties to traverse. For instance, ["year"], whereby the introduction of more elements would result in type errors.

Edit 2: An important note 😅 the provided example portrayed a fictitious interface with two levels of nesting; however, I oversimplified its structure in my inquiry. The actual interface has approximately five levels of nesting. For example, suppose the interfaces Days and Weekend were significantly more complex, with each day containing child objects. My objective is to develop a solution that navigates only two levels deep, disregarding deeper path segments. Therefore, recursive methods may not be viable within this constraint.

Answer №1

If you desire ExamplePath to represent a distributive object type (as introduced in ms/TS#47109) where you distribute the type [K, keyof Example[K]] across each K in the union keyof Example, it will be structured like this:

type ExamplePath = { [K in keyof Example]: [K, keyof Example[K]] }[keyof Example]
/* type ExamplePath = ["days", keyof Days] | ["weekend", keyof Weekend] | 
  ["year", number | typeof Symbol.iterator | "toString" | "charAt" | 
   "charCodeAt" | "concat" | ... 37 more ... | "padEnd"] */

This provides the desired behavior for "days" and "weekend" (uncertain about "year" as the second element would be keyof string encompassing all string methods and properties, aligning with your intention, nevertheless! 👍😬)

Let's now proceed to test it:

const correctlyErrors: ExamplePath = ["days", "test"]; // error
const allowsCorrectPath: ExamplePath = ["days", "monday"]; // no error
const alsoErrors: ExamplePath = ["weekend", "monday"]; // error

Testing successfully completed.

Playground link to code

Answer №2

To simplify this problem, it's important to break it down into smaller parts. While you were close to the solution, the key is to treat each path separately rather than combining them into one tuple. I've opted to utilize a mapped type in this case, although distributive conditional types could also be an alternative:

type KeyPaths<T> = {
    [K in keyof T]: T[K] extends Record<any, any> ? [K, ...KeyPaths<T[K]>] : [K];
}[keyof T];

type ExamplePath = KeyPaths<Example>;

For each key of T, we check if T[K] is an object. If it is, we delve deeper into the object structure. If not, we return [K].

This approach works for the provided examples and helps to identify errors effectively:

Type '"test"' is not assignable to type '"monday" | "tueday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday" | undefined'.(2322)

Similarly, for the second example:

Type '"weekend"' is not assignable to type '"days"'.(2322)

Interactive Playground


It's also possible to introduce a recursion limit to the type definition:

type KeyPaths<T, Depth extends unknown[]> = Depth extends [] ? [] : {
    [K in keyof T]: T[K] extends Record<any, any> ? [K, ...KeyPaths<T[K], Depth extends [...infer D, any] ? D : never>] : [K];
}[keyof T];

type ExamplePath = KeyPaths<Example, [0, 0]>;

In this scenario, we utilize the length of a tuple to track the remaining recursions until completion.

If the tuple method feels unconventional, numerical values can also be used, allowing us to "decrement" them by indexing into a tuple:

type Decrement<X extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9][X];

type KeyPaths<T, Depth extends number> = Decrement<Depth> extends -1 ? [] : {
    [K in keyof T]: T[K] extends Record<any, any> ? [K, ...KeyPaths<T[K], Decrement<Depth>>] : [K];
}[keyof T];

type ExamplePath = KeyPaths<Example, 2>;

However, this method is constrained by the elements included in the Decrement type. Expanding this capability would require additional type manipulation, which exceeds the scope of this question and is unnecessary.

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

Angular 2: Creating a Reusable Object for Uniform JSON Structures

I am facing an issue with JSON data as I have 3 tables in my database named "dictionary" with the same structure but different column names: {"id":1,"test_1":"test"},{"id":2,"test_1":"lalala"} - first JSON {"id":1,"test_2":"****"},{"id":2,"test_2":"afe ...

Can Cloud Functions be used to establish a connection between Cloud Firestore and Realtime Database?

My current project involves designing my firebase database with a unique approach. I am looking to establish a connection between certain document fields in firestore and real-time database fields. This way, any changes made in the real-time database will ...

Encountering ExpressionChangedAfterItHasBeenCheckedError during ngOnInit when utilizing Promise

I have a simple goal that I am working on: I want to display data obtained from a service in my component. This is how it used to work: In my component: ... dataSet: String[]; ngOnInit(){ this._service.getDataId().then(data => this.dataSet = da ...

You are not able to use *ngIf nested within *ngFor in Angular 2

I am currently working in Angular2 and trying to bind data from a service. The issue I am facing is that when loading the data, I need to filter it by an ID. Here is what I am trying to achieve: <md-radio-button *ngFor="#item of items_list" ...

I am facing an issue with my useFetch hook causing excessive re-renders

I'm currently working on abstracting my fetch function into a custom hook for my Expo React Native application. The goal is to enable the fetch function to handle POST requests. Initially, I attempted to utilize and modify the useHook() effect availab ...

Converting Javascript tools into Typescript

I'm currently in the process of migrating my Ionic1 project to Ionic2, and it's been quite an interesting journey so far! One challenge that I'm facing is how to transfer a lengthy list of utility functions written in JavaScript, like CmToFe ...

Directly within Angular, map the JSON data to an object for easy assignment

I came across this information on https://angular.io/guide/http. According to the source, in order to access properties defined in an interface, it is necessary to convert the plain object obtained from JSON into the required response type. To access pr ...

Update the component to display the latest information from the Bryntum grid table

In the Vue component, I have integrated a Bryntum grid table along with a bar chart. Clicking on one of the bars in the chart should update the data displayed in the Bryntum grid table. However, I've encountered difficulty in reloading the entire Bryn ...

Converting Mat-Raised-Button into an HTML link for PDF in Angular 6 using Material Design Library

Greetings, I have a couple of interrelated inquiries: Upon clicking the code snippet below, why does <a mat-raised-button href="../pdfs/test.pdf"></a> change the URL (refer to image 4) instead of opening test.pdf in a new window? If I have a ...

Limiting querySelector to a specific React component: a step-by-step guide

Is there a way to target a specific DOM element within a React component to change its color using the ComponentDidMount method? Parent component export class ListComponent extends Component<...> { render(): ReactNode { return ( ...

Utilizing the useContext hook within a strictly Typescript-based class component

I have developed a pure Typescript class that serves as a utility class for performing a specific task. Within this class, I have created a context that is intended to be used universally. My goal is to utilize this context and its values within the pure T ...

Guide to aligning a component in the middle of the screen - Angular

As I delve into my project using Angular, I find myself unsure about the best approach to rendering a component within the main component. Check out the repository: https://github.com/jrsbaum/crud-angular See the demo here: Login credentials: Email: [e ...

Switch between classes when hovering over / exiting ngFor elements

Displayed below is an element created using ngFor <span *ngFor="let picture of pictures; let i = index"> <a target="_blank" href="{{picture.image}}" class="thumbnail-display image-overlay"> <span class="overlay-icon hide"> ...

AngularJS UI-Router in hybrid mode fails to recognize routes upon initial page load or reload

It appears that when using the @ui-router/angular-hybrid, routes registered within an ng2+ module are not being recognized during the initial load or reload. However, these same routes work fine when accessed by directly typing the URL. I have followed th ...

Difficulty in toggling the visibility of the react-date-range picker package when selecting a date

I need assistance with a problem I'm facing. I am having trouble hiding and showing the react-date-range picker upon date selection. The issue is related to a package that I am using for date range selection. You can find the package link here - https ...

"Learn how to trigger an event from a component loop up to the main parent in Angular 5

I have created the following code to loop through components and display their children: parent.component.ts tree = [ { id: 1, name: 'test 1' }, { id: 2, name: 'test 2', children: [ { ...

Error: The AWS amplify codegen is unable to locate any exported members within the Namespace API

Using AWS resources in my web app, such as a Cognito user pool and an AppSync GraphQL API, requires careful maintenance in a separate project. When modifications are needed, I rely on the amplify command to delete and re-import these resources: $ amplify r ...

What is the best way to convert a `readonly string[]` to a regular `string[]`?

My data setup is as follows (I am not declaring it as an enum because it is used in both TypeScript server code and non-TypeScript client code): import { enumType } from 'nexus'; export const TYPE_ENUM = Object.freeze({ H: 'H', S: ...

What is the best method to create a TypeScript dictionary from an object using a keyboard?

One approach I frequently use involves treating objects as dictionaries. For example: type Foo = { a: string } type MyDictionary = { [key: string]: Foo } function foo(dict: MyDictionary) { // Requirement 1: The values should be of type Foo[] const va ...

Iterate over the key-value pairs in a loop

How can I iterate through a key-value pair array? This is how I declare mine: products!: {[key: string] : ProductDTO}[]; Here's my loop: for (let product of this.products) { category.products.push((product as ProductDTO).serialize()); } However, ...