Using TypeScript to automatically deduce the output type of a function by analyzing the recursive input type

I am currently working on developing an ORM for a graph database using TypeScript. Specifically, I am focusing on enhancing the "find" method to retrieve a list of a specific entity. The goal is to allow the function to accept a structure detailing the joins to be performed at the database level. Moreover, I aim to automatically type these additional fields for easy client access. While I have successfully implemented this feature with single-level nesting, my ultimate aim is to extend it to multiple levels.

Here is how I have achieved functionality for one nesting level:

interface IDocumentModel {
  _id?: string;
}

type JoinParams<T extends Record<string, IDocumentModel>> = {
  [K in keyof T]: {
    model: DocumentModel<T[K]>;
  };
};

type JoinResult<T, U> = (U & {
  [K in keyof T]: T[K][];
})[];

class DocumentModel<T extends IDocumentModel> {
  async find<X extends Record<string, IDocumentModel>>(
    filter?: Partial<T>,
    hydrate?: JoinParams<X>,
  ): Promise<JoinResult<X, T>> {
    // TODO: implementation
  }
}

const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();

const results = await ParentModel.find(
  { _id: 'abc' },
  {
    children: {
      model: ChildModel,
    },
  },
);

console.log(results[0].parentField);
console.log(results[0].children[0].childField);

Now, the challenge lies in extending this functionality to two levels or even beyond. Here is my progress on implementing two levels of nesting:

Below is my current approach to addressing this issue:

interface IDocumentModel {
  _id?: string;
}

type JoinParams<
  T extends
    | Record<string, IDocumentModel>
    | Record<string, Record<string, IDocumentModel>>,
> = {
  [K in keyof T]: {
    model: T extends Record<string, Record<string, IDocumentModel>>
      ? DocumentModel<T[K]['parent']>
      : T extends Record<string, IDocumentModel>
      ? DocumentModel<T[K]>
      : never;
    hydrate?: T extends Record<string, Record<string, IDocumentModel>>
      ? JoinParams<Omit<T[K], 'parent'>>
      : never;
  };
};

type JoinResult<
  T extends
    | Record<string, IDocumentModel>
    | Record<string, Record<string, IDocumentModel>>,
  U,
> = (U & {
  [K in keyof T]: T extends Record<string, Record<string, IDocumentModel>>
    ? JoinResult<Omit<T[K], 'parent'>, T[K]['parent']>
    : T extends Record<string, IDocumentModel>
    ? T[K][]
    : never;
})[];

class DocumentModel<T extends IDocumentModel> {
  async find<X extends Record<string, Record<string, IDocumentModel>>>(
    filter?: Partial<T>,
    hydrate?: JoinParams<X>,
  ): Promise<JoinResult<X, T>> {
    // TODO: implementation
  }
}

const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const GrandChildModel = new DocumentModel<{ _id?: string, grandChildField: string }>();

const results = await ParentModel.find(
  { _id: 'abc' },
  {
    children: {
      model: ChildModel,
      hydrate: {
        grandchildren: {
          model: GrandChildModel,
        },
      },
    },
  },
);

console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);

Upon testing, I noticed that autocomplete suggestions do not go beyond results[0].parentField. This means that the IDE does not recognize results[0].children as a valid field anymore.

I believe this provides sufficient information, but I am open to providing more clarity if needed.

Answer №1

When dealing with TypeScript and trying to infer the generic type argument T from a value of type JoinParams<T>, especially in cases where JoinParams is a recursive conditional type, TypeScript may struggle to make such inferences due to the complexity of the type function involved. In situations like this, it's better to simplify the relationship between the hydrate parameter and the generic type parameter you're attempting to infer.

One simple approach involves making the hydrate parameter directly related to the generic type parameter you want to infer. For instance, if you are aiming to infer H from hydrate, then define hydrate as type H. This way, computations involving other types can be derived from H.

To illustrate this concept, consider the following code snippet:

// JavaScript code example
const user = new DocumentModel({
  _id: 'abc',
  name: 'John Doe'
});

user.findRelatedPosts()
  .then(posts => console.log('User\'s posts:', posts))
  .catch(err => console.error('Error finding related posts:', err));

By simplifying the relationships between different parameters and types, we can improve TypeScript's ability to infer types accurately. This not only enhances code readability but also makes maintenance and debugging easier.

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

Typedoc does not create documentation for modules that are imported

Whenever I generate documentation with TypeDoc, I am encountering an issue where imported files come up empty. If I add a class to the file specified in entryPoints, I get documentation for that specific class. However, the imported files show no document ...

Issues with implementing Firebase Phone Authentication in Ionic 3

When trying to authenticate a phone number in Ionic 3 using Firebase, the program runs without error. However, after entering the phone number, nothing happens... The .html code is shown below: <ion-item> <ion-label stacked>Phone Number</i ...

Is there a way to verify if the database has been successfully saved and the API call has been

I am currently in the process of developing a function that calls two other functions. Function 1 is responsible for saving an object to a database, while function 2 performs an API call. async createMSCalendarEntry(start: Date, end: Date, name: string ...

Troubleshooting Angular modal fade not functioning

I am facing an issue while trying to display a component called "Login", which belongs to the class "modal fade", from another component named "navbar". Despite my attempts to trigger it by calling data-bs-toggle="modal" data-bs-target="#LoginModal" from t ...

The TypeScript reflection system is unable to deduce the GraphQL type in this case. To resolve this issue, it is necessary to explicitly specify the type for the 'id' property of the 'Address'

import { ObjectType, ID, Int, Field } from 'type-graphql'; @ObjectType() export default class Address { @Field(type => ID) id: String; @Field() type: string; @Field() title: string; @Field() location: string; } More informa ...

Angular 2: The linting error shows up as "Anticipated operands need to be of the same type or any"

So, I have this shared service file where a variable is defined like so: export class SharedService { activeModal: String; } Then, in my component file, I import the service and define it as follows: constructor(public sharedService: SharedService) ...

SystemJS could not locate the root directory for RxJS

There seems to be an issue with SystemJS loading rxjs modules on Windows, as it throws a 404 Not Found error on the rxjs directory. This problem does not occur on OSX, and all modules are up to date. GET http://localhost:8080/node_modules/rxjs/ 404 (Not F ...

iterating over a nested map within a map in an Angular application

I wrote a Java service that returns an observable map> and I'm currently struggling to iterate through the outer map using foreach loop. [...] .then( (response: Package) => { response.activityMap.forEach((key: string, value ...

Oops! There seems to be an issue with the code: "TypeError: this

I am just starting out with Angular. Currently, I need to assign a method to my paginator.getRangeLabel (I want to use either a standard label or a suffixed one depending on certain conditions): this.paginator._intl.getRangeLabel = this.getLabel; The cod ...

What is the source of the compiler options in tsconfig.json?

Currently utilizing Typescript in NestJs, I have incorporated various packages. However, the specific package responsible for altering these settings remains unknown to me: "checkJs": false, "skipLibCheck": true Is there a method to ...

How can I pass additional props that are not specified in the interface while working with a React TS component?

I am working with a TS React component called MyButton.tsx: import React from 'react' interface MyButtonProps { children: JSX.Element | JSX.Element[], className?: string, variant?: 'big-button' | 'medium-button' | &apos ...

I am seeking advice on how to create an extension for a generic class in TypeScript specifically as a getter

Recently, I discovered how to create extensions in TypeScript: interface Array<T> { lastIndex(): number } Array.prototype.lastIndex = function (): number { return this.length - 1 } Now, I want to figure out how to make a getter from it. For exam ...

Unable to simulate axios instance in a Typescript environment

After reading through this particular article, I decided to attempt writing a unit test while simulating Axios (with Typescript). Incorporating an Axios instance to define the baseUrl. // src/infrastructure/axios-firebase.ts import axios from 'axios ...

How can we avoid excessive re-rendering of a child component in React when making changes to the parent's state?

In my React application, I am facing a situation where a parent component controls a state variable and sends it to a child component. The child component utilizes this state in its useEffect hook and at times modifies the parent's state. As a result, ...

Looking for a regular expression to verify if the URL inputted is valid in TypeScript

After conducting thorough research, I discovered that none of the suggested URLs met my criteria, prompting me to raise a new query. Here are my specific requirements: * The URL may or may not include 'http' or 'https' * The URL can co ...

Developing a search feature using Angular 6 with Observable subscription for the FrontEnd application

I have a unique challenge where I need to implement a full text search in the FrontEnd due to restrictions with the API. When the frontend starts up, it fetches all data entries from the Backend and subscribes them inside a component using an async pipe. T ...

What is the equivalent of defining conditional string types in Typescript similar to flow?

type UpsertMode = | 'add' | 'update' | 'delete'; interface IUpsertMembers { mode: UpsertMode; } const MagicButton = ({ mode: UpsertMode }) => { return ( <button>{UpsertMode}</button> ); } const Upse ...

There is no initial value set for the property and it is not definitively assigned in the constructor

I encountered an issue while working on the following code snippet: export class UserComponent implements OnInit { user: User; constructor() { } ngOnInit() { this.user = { firstName : "test", lastName ...

What causes the createResource error to become undefined when it is refetched before being properly set?

How can I access and display the error property for a createResource? In this scenario, why is the error initially set to undefined before it is thrown? The logging shows that it first displays undefined for the error before eventually getting to line er ...

Encountering a problem with Vue StripeCheckout while navigating to a different component

I'm looking to integrate the StripeCheckout component into my Vue application. After copying and updating their example code using the composition API from here, everything works fine when I route to the subscribe component. However, if I try to navig ...