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

TypeScript observable variable not defined

Recently, I encountered an issue and made a change to resolve it. However, I am unsure if it is the correct approach... In my project, I have defined an interface: export interface ContextEnvironment { language: string; pingUrl: string; sessionFini ...

Simulating Express Requests using ts-mockito in Typescript

Is there a way to simulate the Request class from Express using ts-mockito in typescript? I attempted the following import { Request, Response } from "express"; const request = mock(Request); const req: Request = instance(request); but encou ...

What is the process of using TypeScript to import a constant exported in JavaScript?

In the environment I'm using typescript 2.6.1. Within react-table's index.js file, there is a declaration that looks like this: import defaultProps from './defaultProps' import propTypes from './propTypes' export const React ...

Improving DynamoDb Query Results with Type Hinting

In the following Typescript code, how can I specify which fields should be present in my Query Items Result? const request: DynamoDB.DocumentClient.QueryInput = { TableName: UnsubscriptionTokensRepository.TABLE_NAME, IndexName: 'TokenIndex&ap ...

Best practices and distinctions when it comes to typing in TypeScript functions

Do the typings below differ in any way, or are they essentially the same with personal preference? interface ThingA{ myFunc(): number; } interface ThingB{ myFunc: () => number; } ...

What is the best way to implement a timer using hooks in React?

Just getting started with React! I began my journey last week ;) My first task is to build a timer that includes a reset feature and can count seconds. While the reset function is functioning properly, the timer isn't. Can anyone suggest the best ap ...

Creating an empty TypeScript variable with type FileList can be achieved by declaring the variable and initializing it with

After completing a coding test that required building a react app for uploading files using Typescript, I encountered a dilemma. Specifically, I needed to use the useState hook to store the uploaded file and set its default value. Typically, setting the de ...

Unlocking the power of global JavaScript variables within an Angular 2 component

Below, you will find a global JavaScript variable that is defined. Note that @Url is an ASP.Net MVC html helper and it will be converted to a string value: <script> var rootVar = '@Url.Action("Index","Home",new { Area = ""}, null)'; Sy ...

The offline functionality of the Angular Progressive Web App(PWA) is experiencing difficulties

As per the official guidelines, I attempted to create a PWA that functions in offline mode using pure PWA without angular-cli. However, despite following the official instructions, I was unable to make it work offline. The document in question can be foun ...

Issue - Unrecognized listen EADDRINUSE :::5432 detected in Windows Command Prompt

I encountered an issue when I tried running gulp serve --nobrowser, resulting in the following error: { Error: listen EADDRINUSE :::5432 at Object._errnoException (util.js:992:11) at _exceptionWithHostPort (util.js:1014:20) at Server.setupListenHandle [as ...

Implementation of a recursive stream in fp-ts for paginated API with lazy evaluation

My objective involves making requests to an API for transactions and saving them to a database. The API response is paginated, so I need to read each page and save the transactions in batches. After one request/response cycle, I aim to process the data an ...

Setting IDPs to an "enabled" state programmatically with AWS CDK is a powerful feature that allows for seamless management of

I have successfully set up Facebook and Google IDPs in my User Pool, but they remain in a 'disabled' state after running CDK deploy. I have to manually go into the UI and click on enabled for them to work as expected. How can I programmatically e ...

Can you explain how to utilize prop values for setting attributes in styled-components v4 with TypeScript?

Overview Situation: const Link = styled.a` border: solid 1px black; border-radius: 5px; padding: 5px; margin: 10px 5px; `; type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>; const LinkAsButton = styled(Link).attrs<ButtonP ...

What is the best way to organize code within the main.ts file in a Vue 3 project?

New to Typescript and vue, I am eager to figure out how I can extract this code from my main.ts file. I'm concerned about it becoming messy as more icons are added. const app = createApp(App); /* import the fontawesome core */ import { library } from ...

Do you find this unattractive? What are some ways to improve this unsightly JavaScript statement?

This code seems messy, how can I better structure this switch statement? function renderDataTypeIcon(dataType: string) { let iconName; switch (dataType) { case "STRING": //TODO - ENUM iconName = "text"; break; ...

Access-Control-Allow-Methods does not allow the use of Method PUT in the preflight response, as stated by Firebase Cloud Functions

I am facing an issue with my Firebase cloud function endpoint. I have a setup where it forwards PUT requests to another API endpoint. I have configured the following Access-Control-Allow- headers: // src/middlewares/enableCORS.ts export default function en ...

I'm experiencing difficulty in scrolling on my Nextjs web application

Currently, I am facing an issue with my portfolio webpage which is divided into 3 main components - Hero, About, and Portfolio. The layout structure is as follows: export default function RootLayout({ children, }: { children: React.ReactNode }) { ret ...

The state may be modified, but the component remains unchanged

I've been tasked with implementing a feature on a specific website. The website has a function for asynchronously retrieving data (tickets), and I need to add a sorting option. The sorting process occurs on the server side, and when a user clicks a s ...

Why Mixin Class inference is not supported in Typescript

I encountered an issue in my code: The error message 'Property 'debug' does not exist on type 'HardToDebugUser'.' was displayed. It seems like Typescript failed to infer the mixin class correctly. Can you please explain this t ...

Angular2's asynchronous data binding is still lagging even after the data has been successfully loaded

I have integrated Firebase with Angular2 to retrieve an object. import { Component, OnInit } from '@angular/core'; import { AngularFire, FirebaseObjectObservable } from 'angularfire2'; import { ActivatedRoute, Params } from '@angu ...