Understanding class declaration within a dynamic context in TypeScript

I am currently working on a library and facing difficulties with dynamically inferring types.

Within the library, the useModel function returns a Model instance.

class Database {
  ...
  public useModel(target: Function) {
    const tableName = getClassMetadata(target).name;
    const table = this.config.tables.find(({ name }) => name === tableName);

    // The issue lies here - type is not being inferred
    type RType = typeof target extends (...args: any[]) => infer RT ? RT : Function;
    // Model has selectAll() which returns RType[]
    return new Model<RType>(this.connection, table);
  }
}

Below is an example of how the library would be used by a consumer.

Custom decorators are used to add metadata values.

@Table({ name: 'Users', timestamps: true })
class Users {
  @PrimaryKey({autoIncrement: true, unique: true,})
  id: number;

  @Indexed({ unique: true })
  @Default('John')
  username: string;

  @Default(0)
  @Indexed({ unique: false })
  age: number;
}

const database = new Database({
  version: 1,
  name: 'MyDatabase',
  tables: [Users],
});

// The goal is for the Users class to be automatically inferred in useModel
const usersModel = database.useModel(Users);
usersModel.selectAll().then((users) => {
  users?.forEach((user) => {
    // user is not being inferred here :( 
    console.log(user);
  });
});

Using generics in useModel would solve the issue, but the desire is to avoid explicit generic values like

const usersModel = database.useModel<typeof Users>(Users);

The objective is for the Users class to be inferred automatically when using useModel

This is the expected usage

const usersModel = database.useModel(Users);

Any suggestions to achieve this?

Answer №1

To achieve this, you simply need a generic type parameter instead of a conditional type. Here is an example:

class Database {

  useModel<T>(target: new () => T) {
    const tableName = getClassMetadata(target).name;
    const table = this.config.tables.find(({ name }) => name === tableName);
    
    return new Model<T>(this.connection, table);
  }
}

The reason for the failure of the original version is because conditional types need to be determined at compile time and cannot rely on a specific execution path.

Regarding type inference at the call site, as you mentioned:

If I use generics in useModel, I know it will work, but I don't want to specify the generic value like

const usersModel = database.useModel<typeof Users>(Users);

An API that requires explicit type arguments is not just inconvenient, but it also indicates a flawed design and is prone to errors.

const usersModel = database.useModel(Users);

This approach is fully supported by the code above and is recommended for better readability and correctness. Explicitly passing type arguments at call sites is not a good practice. It often signifies a design flaw. A generic function is only meaningful when all type parameters can be inferred from the type of a value parameter. In this case, the value is target, and T is the type constructed by newing target.

Additional Comments

The implementation above can be simplified further to prevent misuse, assuming that config.tables is an array of constructors.

Consider:

class Database {
  useModel<T>(target: new () => T) {
    if (!this.config.tables.includes(target)) {
      throw Error("Unconfigured Table");
    }

    return new Model(this.connection, target);
  }
}

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

Connect AngularFire to a specific object

I'm facing an issue with my Users.class where I need it to automatically map or bind after fetching data from Firebase. I've been trying to search for the right term but haven't found any information yet. import { Component, OnInit } from & ...

When implementing Angular 6, using a shared module within individual lazy-loaded modules can lead to a malfunctioning app when changes are

Hey there, I've encountered a strange problem that didn't occur when I was using Angular 5. Let me explain the situation: In my App routing module, I have: { path: 'moduleA', pathMatch: 'full', loadChildren: &ap ...

React with TypeScript - Troubleshooting TS Error when Iterating over List Item (LI) Elements

Iterating through a group of <li> elements to strip away a specific class, then applying the same class to a different <li>. See the snippet below: useEffect(() => { if (dnArrowIdx === undefined) { const allLi = Array.from(document. ...

Encountering an Issue with Dynamic Imports in Cypress Tests Using Typescript: Error Loading Chunk 1

I've been experimenting with dynamic imports in my Cypress tests, for example using inputModule = await import('../../__tests__/testCases/baseInput'); However, I encountered an issue with the following error message: ChunkLoadError: Loading ...

Convert data into a tree view in JavaScript, with two levels of nesting and the lowest level represented as an array

Here is an example of a JSON object: [ { "venueId": "10001", "items": [ { "venueId": "10001", "locationId": "14", "itemCode": "1604", "itemDescription": "Chef Instruction", "categoryCode": "28", ...

Align watermark content to the far left side

Having trouble getting my watermark to align properly on the left side of my website's main content. Here is how it currently looks: https://i.sstatic.net/Nfhh5.png The issue arises when I resize the screen or switch to mobile view, as the watermark ...

The functionality of the Ionic 4 app differs from that of an Electron app

I've encountered an issue with my Ionic 4 capacitor app. While it functions properly on Android studio, I'm having trouble getting it to work on Electron. Any ideas on how to resolve this? Here are the steps I took to convert it to Electron: np ...

Angular 9: Chart.js: Monochromatic doughnut chart with various shades of a single color

My goal is to display a monochromatic doughnut chart, with each segment shaded in varying tones of the same color. I have all the necessary graph data and just need to implement the color shading. ...

Unable to successfully import Node, JS, or Electron library into Angular Typescript module despite numerous attempts

I'm still getting the hang of using stack overflow, so please forgive me if my question isn't formulated correctly. I've been doing a lot of research on both stack overflow and Google, but I can't seem to figure out how to import Electr ...

Exploring the incorporation of an inclusive switch statement within a Redux reducer utilizing Typescript. Strategies for managing Redux's internal @@redux actions

After conducting extensive research, I have yet to discover a definitive answer to this query. There is a question posted on Stack Overflow that provides guidance on how to implement a thorough switch statement: How can I ensure my switch block covers al ...

Is there a workaround for utilizing reducer dispatch outside of a React component without relying on the store?

Recently, I implemented a reducer in my project that involves using react, typescript and nextJS. I am wondering if there is a method to trigger the reducer outside of a react component, such as from an API service. While searching for solutions, most re ...

You cannot call this expression. The data type 'Boolean' does not have any callable signatures

As I delve into learning a new set of technologies, encountering new errors is inevitable. However, there is one particular type of error that keeps cropping up, making me question if I am approaching things correctly. For instance, I consistently face t ...

What is the best way to create a custom type guard for a type discriminator in TypeScript?

Suppose there are objects with a property called _type_ used to store runtime type information. interface Foo { _type_: '<foo>'; thing1: string; } interface Bar { _type_: '<bar>' thing2: number; } function helpme(i ...

Customizing a side menu by assigning a function to a specific option

Hello there! I am a newcomer to TypeScript and Ionic. I am trying to implement a function that clears the cart when the "Mercado" option in the side menu is clicked, but I am struggling to retrieve the page data. The code snippet below shows my attempt to ...

Tips for creating dynamic amd-dependencies in TypeScript

Is there a way to dynamically load a Javascript language bundle file in Typescript based on the current language without using static methods? I want to avoid having to use comments like this for each bundle: /// <amd-dependency path="<path_to_bund ...

Utilizing Service within Express Router

My journey into the world of NodeJS is just beginning, coming from a .NET background with a love for dependency injection, inversion of control, and microservices. I am now venturing into TypeScript to create services based on my past experiences. Using ex ...

a callback may seem like it's not a function, but in reality, it is

This question might seem simple, but I'm struggling to grasp the concept of callbacks, especially in NodeJS. My issue arises when trying to retrieve data from MySQL, something that is usually straightforward in most programming languages: In my rout ...

Transform array sequences into their own unique sequences

Reorder Array of List to Fit My Custom Order Current Output: [ { "key": "DG Power Output", "value": "6.00", "unit": "kWh", }, { "key": "DG Run Time", "value": "5999999952", "unit": "minutes", }, { "key": "Fuel Level (Before)", "value": "8.00" ...

Dealing with Uncaught Promises in Angular 2 while injecting a service

I followed the instructions in the official tutorial to start a project, but I'm encountering an issue with injecting services into my Angular2 app. Everything was working fine until I added a service. Here are the files : app.component.ts import ...

What is the purpose of having a tsconfig.json file in every subdirectory, even if it just extends the main configuration file?

My goal is to streamline the configuration files in my front-end mono repo by utilizing Vite with React and TypeScript. At the root of my repository, I have set up a tsconfig.json file that contains all the necessary settings to run each project, including ...