How can I define the type of a constructor that requires a parameter in TypeScript?

Having identified the issue, let's focus on a minimal example:

// interfaces:

    interface ClassParameter{
        x:number
    }

    interface ClassParameterNeeder{
        y:number
    }

    type ClassParameterConstructor = new () => ClassParameter

    type ClassParameterNeederConstructor =
        new (cpc: ClassParameterConstructor) => ClassParameterNeeder

    // implementations:

    class MyClassParameter implements ClassParameter{
        x = 12
        extraField = 27
    }

    class MyClassParameterNeeder implements ClassParameterNeeder{
        y = 19
        otherExtraField = 29
        constructor(mcpc: new() => MyClassParameter) {}
    }

    let doesntWork:ClassParameterNeederConstructor = MyClassParameterNeeder
    

Regarding TypeScript's complaint about the final line:

let doesntWork: ClassParameterNeederConstructor
    Type 'typeof MyClassParameterNeeder' is not assignable to type 'ClassParameterNeederConstructor'.
      Types of parameters 'mcpc' and 'cpc' are incompatible.
        Type 'ClassParameterConstructor' is not assignable to type 'new () => MyClassParameter'.
          Property 'extraField' is missing in type 'ClassParameter' but required in type 'MyClassParameter'.(2322)
    input.ts(22, 5): 'extraField' is declared here.
    

This situation led me to question the reversed dependency, which actually makes sense. In the definition of type ClassParameterNeederConstructor, an outer Constructor complying with that type must accept an inner ClassParameterConstructor capable of constructing any kind of ClassParameter, not just the specialized constructor for MyClassParameter. But then, how do I resolve this? It seems impossible to narrow the interface in the implementation, and the implementation cannot accept any kind of ClassParameterConstructor without specific requirements for MyClassParameter constructors...

Any assistance would be greatly appreciated!

EDIT:

Attempting to apply @jcalz's solution to my specific issue: Playground

Previously, I tried using generics but placed the <> after the new (line 26):

RigidBody: new<S extends ShapeInterface> (shapeClass: new () => S) => RigidBodyInterface<S>
    

Unfortunately, that didn't work. I have now moved it up into the constructor of PhysicsInterface (now on line 23) and it functions correctly.

However, I must now list all the abstract classes used by the engine in the generics parameter list, which is a bit cumbersome (especially since TypeScript doesn't display generics parameters as tooltips), but it seems necessary.

Another attempt was made to create a type-inferring factory, but it ultimately failed (the last line throws an error), and it's also quite confusing. I would also need a mechanism to inject engine-specific implementations for the physics engine class into the physics engine class factory, so I'll stick with method A.

Currently, the implementation allows a physics engine user to access the p2-specific members of the classes and they are visible in TypeScript code completion. Is there a way to keep that private while still allowing the engine itself to access all p2 fields? Something akin to using friend in C++...

I have a functional solution now, but any input on making the factory work or about the concept in general is appreciated.

Thank you!

Answer №1

Your MyClassParameterNeeder constructor code seems to be expecting a MyClassParameter constructor instead of an instance. To correct this, annotate mcpc as type typeof MyClassParameter or as new () => MyClassParameter:

class MyClassParameterNeeder implements ClassParameterNeeder {
   y = 19
   otherExtraField = 29
   constructor(mcpc: new () => MyClassParameter) { } // updated annotation
}

After this correction, another issue arises where MyClassParameterNeeder is not a valid ClassParameterNeederConstructor. This aligns with the underlying problem:

let doesntWork: ClassParameterNeederConstructor = MyClassParameterNeeder
//Type 'typeof MyClassParameterNeeder' is not assignable to type 'ClassParameterNeederConstructor'.
//Types of parameters 'mcpc' and 'cpc' are incompatible.
//Type 'new () => ClassParameter' is not assignable to type 'new () => MyClassParameter'.
//Property 'extraField' is missing in type 'ClassParameter' but required in type 'MyClassParameter'.

Your implementation of MyClassParameterNeeder specifies that mcpc must create instances of MyClassParameter exclusively. Therefore, the existing ClassParameterNeederConstructor type is not suitable. Consider a scenario with a new subclass:

class AnotherClassParameter implements ClassParameter {
   x = 12
   foo = true
}

class AnotherClassParameterNeeder implements ClassParameterNeeder {
   y = 19
   bar = false
   constructor(mcpc: new () => AnotherClassParameter) { }
}

It's imperative to maintain distinction between MyClassParameterNeeder and AnotherClassParameterNeeder without losing essential information. Attempting to create a common type may lead to either safety concerns or inconclusive outcomes:

type UselessClassParameterNeederConstuctor =
   new (cpc: new () => never) => ClassParameterNeeder

const uselessMine: UselessClassParameterNeederConstuctor = MyClassParameterNeeder;
const uselessAnother: UselessClassParameterNeederConstuctor = AnotherClassParameterNeeder;
new uselessMine(MyClassParameter); // error

Alternatively, a generic approach for ClassParameterNeederConstructor based on the generics concept is recommended:

type ClassParameterNeederConstructor<C extends ClassParameter> =
   new (cpc: new () => C) => ClassParameterNeeder

const genericMine: ClassParameterNeederConstructor<MyClassParameter> =
  MyClassParameterNeeder;
const genericAnother: ClassParameterNeederConstructor<AnotherClassParameter> =
  AnotherClassParameterNeeder;

new genericMine(MyClassParameter); // works
new genericAnother(AnotherClassParameter); // works
new genericMine(AnotherClassParameter); // error

This resolves the key issues!


To simplify the annotation process, a helper function can be used to automatically infer the type:

const asClassParameterNeederConstructor =
   <C extends ClassParameter>(c: ClassParameterNeederConstructor<C>) => c;

const inferredMine = asClassParameterNeederConstructor(MyClassParameterNeeder);
const inferredAnother = asClassParameterNeederConstructor(AnotherClassParameterNeeder);
new inferredMine(MyClassParameter); // works
new inferredAnother(AnotherClassParameter); // works
new inferredMine(AnotherClassParameter); // error

The types of inferredMine and inferredAnother will be inferred automatically based on usage. Good luck with your implementation!


For a live example, check the Playground link

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

Slate - developing a TypeScript function to filter and retrieve an object containing the highest property value

Check out this NEW RELATED QUESTION: I need to extract the largest number from a given object set. I am struggling with finding a solution. I have tried using max but I think my skills are lacking. Here is the code I have so far: @Function() pub ...

An object resulting from the combination of two separate objects

After reading a helpful solution on StackOverflow about merging properties of JavaScript objects dynamically, I learned how to utilize the spread operator in Typescript. However, one question still remains unanswered - what will be the type of the object c ...

Angular 2 code test coverage

Looking to calculate the code coverage of my Angular 2 code. Wondering if there are any plugins available for VS Code or WebStorm that can assist with this. My unit testing is done using Jasmine and Karma. ...

"Error: The property $notify is not found in the type" - Unable to utilize an npm package in Vue application

Currently integrating this npm package for notification functionalities in my Vue application. Despite following the setup instructions and adding necessary implementations in the main.ts, encountering an error message when attempting to utilize its featur ...

Tips on displaying the entire text when hovering over it

I'm facing an issue with a select element that retrieves options from an API. The problem is that some options are too large for the text box and get cut off, making them hard to read for users. <div class="form-group my-4"> <lab ...

A guide to successfully transferring data array values from a Parent Component to a Child Component using APIs in Angular

To transfer values of this.bookingInfo = bookings.responseObj.txnValues; array from the parent component to the bookingInfo array in my child component and then insert that data into the chartNameChartTTV.data = []; array in the child component. Here, divN ...

Create a specific definition for a packaged item with limited access

As I worked with a package that lacked types, I took the initiative to create a type definition for it. The specific package in question is react-date-range. In order to accommodate this new type definition, I established a directory where it could be loca ...

Expanding Mongoose Schema with Typescript: A Comprehensive Guide

Currently, I am in the process of creating 3 schemas (article, comment, user) and models that share some common fields. For your information, my current setup involves using mongoose and typescript. Mongoose v6.1.4 Node.js v16.13.1 TypeScript v4.4.3 Eac ...

Retrieve the property of a Typescript object using a template argument

I am looking to develop a Typescript Collection class that can locate items by field. Here is an example of what I have in mind: class Collection<T, K keyof T> { private _items: T[]; public isItemInCollection(item: T) { return _item ...

Adding additional properties to Material UI shadows in Typescript is a simple process that can enhance the visual

https://i.stack.imgur.com/9aI0F.pngI'm currently attempting to modify the Material UI types for shadows, but encountering the following error when implementing it in my code. There is no element at index 25 in the tuple type Shadows of length 25. I&a ...

Do we really need Renderer2 in Angular?

Angular utilizes the Renderer2 class to manipulate our view, acting as a protective shield between Angular and the DOM, making it possible for us to modify elements without directly interacting with the DOM ourselves. ElementRef provides another way to al ...

Creating a grid UI in AngularJS using Typescript: utilizing functions as column values

I am working on an AngularJS app that includes the following UI grid: this.resultGrid = { enableRowSelection: true, enableRowHeaderSelection: false, enableHorizontalScrollbar: 0, enableSorting: true, columnDefs: [ { name: &apos ...

Swapping Out Imports with Window Objects in TypeScript

After creating a TypeScript module that relies on a third-party library, the JavaScript output from compilation includes a statement using require: "use strict"; var dexie_1 = require("dexie"); var storage; (function (storage) { ... })(storage || (stora ...

What is the best way to test the SSM getParameter function using Jasmine?

Is there a way to effectively test this? const ssmParameterData = await ssm.getParameter(params, async (error, data) => { if (error) throw error; return data; }).promise(); I have attempted mocking the method by doing: spyOn(ssm, 'getParameter& ...

The error "Property 'push' does not exist on type '() => void'" occurs with Angular2 and Typescript arrays

What is the method to initialize an empty array in TypeScript? array: any[]; //To add an item to the array when there is a change updateArray(){ this.array.push('item'); } Error TS2339: Property 'push' does not exist on type &a ...

When the *ngFor directive disrupts the CSS Grid Layout, resulting in all items being displayed in a single column

I am a beginner in the world of programming and web development. Currently, I am working on building my own personal website. My goal is to arrange boxes in a grid with 4 columns, similar to the layout you can find at this link: Each box represents an ob ...

Guide on setting up a route in Next.js

Recently, I developed a simple feature that enables users to switch between languages on a webpage by adding the language code directly after the URL - i18n-next. Here's a snippet of how it functions: const [languages, ] = React.useState([{ langua ...

Why does the custom method only trigger once with the addEventListener?

I am attempting to connect the "oninput" event of an input range element to a custom method defined in a corresponding typescript file. Here is the HTML element: <input type="range" id='motivation-grade' value="3" min="1" max="5"> This i ...

Migrating image information from Angular version 14 to Asp.Net Core 6.0 Rest Api

When transferring product model data from Angular to a REST API using FormData, there is an images array included in the product data. Upon receiving this product in the REST API, the images data is accessed using Request.Form.Files. The images are then se ...

TypeScript Error 2304: Element 'div' is nowhere to be found - CRA TypeScript Template

I'm experiencing a problem in VSCode while working on the default create-react-app my-app --template typescript project. It seems to not recognize any HTML elements, as I keep getting the error cannot find name xxx, where 'xxx' represents th ...