Exploring the method for obtaining parameters from a generic constructor

I have a customized class called Collection, which takes another class as a parameter named personClass. I expect the method add to accept parameters that are used in the constructor of the class User

class Person {
  constructor(public data: object) {
  }
}

type GetConstructorParams<T> = T extends {
  new (...params: infer Args): any
} ? Args : never

type GetConstructor<Instance> = new (...params: GetConstructorParams<Instance>) => Instance

class Collection<
  Instance extends Person
> {
  personClass: GetConstructor<Instance>
  items: Instance[] = []

  constructor(personClass: GetConstructor<Instance>) {
    this.personClass = personClass
  }

  add(...params: GetConstructorParams<Instance>) {
    const instance = new this.personClass(...params)
    this.items.push(
      instance
    )
  }
}

class User extends Person {
  data: {
     name: string
  }
  constructor(name: string) {
    super({name})
  }

  get name() {
    return this.data.name
  }
}

const collection = new Collection(User)

collection.add('123')
                ^^^ TS2345: Argument of type 'string' is not assignable to parameter of type 'never'.

Next, I will attempt to make personClass an optional parameter. By default, personClass should be set to Person

How can I prevent errors in the collection.add('123') method?

Answer №1

Instance types of classes in TypeScript do not directly refer to their constructor type. Let's take a look at an example class:

class Foo {
    a: number;
    constructor(a: number) {
        this.a = a;
    }
}

let f: Foo = new Foo(123);

The variable f is of type Foo, but the type Foo should be thought of as an interface that describes the structure of instances of the class, rather than indicating it was constructed by the Foo constructor. You can even assign an object with the same shape to f without being created by Foo:

f = { a: 456 }; // <-- also valid

Even the constructor property doesn't hold enough typing information about the actual constructor of a class instance.

Hence, determining that the constructor named Foo has a constructor parameter of type number solely from the type Foo is impossible and leads to loss of information.

GetConstructor<I> will never function as expected because you cannot convert from the type User to typeof User.


However, constructor types are aware of the instances they create. With the constructor type typeof User, you can obtain its instance type. There is even an InstanceType<T> utility type available for this purpose.

Moreover, if all you need is the instance type and constructor parameter list, make your Collection generic in those types directly instead of focusing on the constructor type itself. For example, assuming the constructor argument types are array-like A and the instance type is similar to Person denoted as T, here's the modified Collection:

class Collection<A extends any[], T extends Person>{
    personClass: new (...args: A) => T;
    items: T[] = []

    constructor(personClass: new (...args: A) => T) {
        this.personClass = personClass
    }

    add(...params: A) {
        const instance = new this.personClass(...params)
        this.items.push(
            instance
        )
    }
}

This code compiles without issues. You can then instantiate User within Collection as planned:

class User extends Person {
    data!: {
        name: string
    }
    constructor(name: string) {
        super({ name })
    }

    get name() {
        return this.data.name
    }
}

const collection = new Collection(User)
// const collection: Collection<[name: string], User>

collection.add('123'); // acceptable

The type of collection becomes

Collection<[name: string], User>
, reflecting the fact that the User constructor accepts a single string parameter and produces instances of type User.

Class instance types in TypeScript do not hold a reference to their constructor type. Consider the following class:

class Foo {
    a: number;
    constructor(a: number) {
        this.a = a;
    }
}

let f: Foo = new Foo(123);

The value f is annotated to be of type Foo. But you should think of the type Foo as an interface describing the shape of class instances, and not as meaning "was constructed by the Foo constructor". Indeed, you can assign a value of the same shape to f without it being constructed by Foo at al:

f = { a: 456 }; // <-- also acceptable

If you look at microsoft/TypeScript#3841 you will see that even the constructor property of a class instance is not strongly typed enough to determine anything about the actual constructor.

So, there's no way of looking at the type Foo and determining that the constructor named Foo has a constructor parameter of type number. It's impossible to go from instance type to constructor type without losing information. GetConstructor<I> will never work how you want. There's no way to go from the type named User to the type named typeof User.


On the other hand, constructor types definitely know about the instances they construct. So given the User constructor of type typeof User, you can get its instance type. There's even an InstanceType<T> utility type provided for you.

Furthermore, since all you care about is the instance type and the constructor parameter list, you can just make your Collection generic in the types of those directly, instead of caring about the constructor type itself. Let's say that the constructor argument types is an arraylike type A and the instance type is a Person-like type T. Then here's how Collection looks:

class Collection<A extends any[], T extends Person>{
    personClass: new (...args: A) => T;
    items: T[] = []

    constructor(personClass: new (...args: A) => T) {
        this.personClass = personClass
    }

    add(...params: A) {
        const instance = new this.personClass(...params)
        this.items.push(
            instance
        )
    }
}

That all compiles with no problem. Then you can pass in User and see everything working as desired:

class User extends Person {
    data!: {
        name: string
    }
    constructor(name: string) {
        super({ name })
    }

    get name() {
        return this.data.name
    }
}

const collection = new Collection(User)
// const collection: Collection<[name: string], User>

collection.add('123'); // okay

The type of collection is

Collection<[name: string], User>
, corresponding to the fact that the User constructor takes a single string parameter and produces instances of type User.

Playground link to code

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

The useState variable's set method fails to properly update the state variable

I am currently developing an application with a chat feature. Whenever a new chat comes in from a user, the store updates an array containing all the chats. This array is then passed down to a child component as a prop. The child component runs a useEffect ...

Having trouble retrieving a value from the img.onload event handler. A 'boolean' type error is being thrown, indicating it cannot be assigned to type '(this: GlobalEventHandlers, ev: Event) => any'

In my Angular application, I have implemented a method that verifies the size and dimensions of an image file and returns either false or true based on the validation result. Below is the code snippet for this function: checkFileValidity(file: any, multipl ...

Exploring the Relationship Between the ngOnInit and ionViewWillLoad Lifecycle Hooks

After a few months of utilizing Ionic Framework (ionic-angular 3.9.2 latest) for developing Progressive Web Apps, I find myself pondering the distinction between ngOnInit and ionViewWillLoad. If my understanding serves me right, ngOnInit is an Angular lif ...

When you extend the BaseRequestOptions, the injected dependency becomes unspecified

Implementing a custom feature, I have chosen to extend BaseRequestOptions in Angular2 to incorporate headers for every request. Additionally, I have introduced a Config class that offers key/value pairs specific to the domain, which must be injected into m ...

"An error occurred stating that _co.JSON is not defined in

During my attempt to convert an object into a string using the JSON method, I encountered an error upon loading my page: Error: _co.JSON is undefined The stacktrace for this error message is extensive and seems unnecessary to include at this point. Th ...

Is there a way for me to steer clear of using optional chaining in Typescript?

I'm currently working on storing object data known as Targetfarms in redux. I've defined a type named Farmstype for the Targetfarms. However, when I retrieve the Targetfarms using useSelector in the MainPage component and try to access targetfar ...

Angular sub-route is failing to activate

My current setup involves Angular routing along with the use of ngx-translate-router, and I've encountered an unusual issue with child routes. It's unclear whether this problem is connected to the translated router module I'm utilizing, but ...

Encrypting and decrypting data using RSA in TypeScript

Currently, I am utilizing Angular 4 to develop the front end of my application. For authentication, I have integrated OAuth2 on the backend (which is created using Spring in Java), ensuring that only authorized individuals can access my app. However, ther ...

Listening for a long press in Angular 2: A step-by-step guide

Check out this link for an AngularJS example. Curious how this functionality translates to Angular 2? ...

Intellisense in VS Code is failing to provide assistance for data within Vue single file components

I am working with a simple code snippet like this However, within the method, the variable 'name' is being recognized as type any. Interestingly, when I hover over 'name' in the data, it shows up as a string. The Vetur plugin has alre ...

Modifying the name of a key in ng-multiselect-dropdown

this is the example data I am working with id: 5 isAchievementEnabled: false isTargetFormEnabled: true name: "NFSM - Pulse" odiyaName: "Pulse or" when using ng-multiselect-dropdown, it currently displays the "name" key. However, I want ...

The module does not contain 'toPromise' as an exported member in rxjs version 5.5.2

Encountering an error when using toPromise Prior method: import 'rxjs/add/operator/toPromise'; Updated approach: import { toPromise } from 'rxjs/operators'; The new way is causing the following issues: [ts] Module '"d:/.../ ...

What's the best way to insert values into data binding within a Typescript/ Angular mat Table?

Objective: Create a dynamic table using JSON data <mat-table class="mat-elevation-z8" *ngIf="carrierRates" [dataSource]="carrierRates"> <ng-container *ngFor="let columnName of columnsList" matColumn ...

The mat-table component in my HTML code is not displaying the dataSource as expected, even though I meticulously copied it from Material Angular

Although I am aware that my question may seem unusual, my issue precisely matches what the title conveys. The problem lies in my mat-table dataSource not displaying any data, even after attempting to log the data with console.log("My Data : ", this.dataSou ...

How can I showcase a different component within another *ngFor loop?

I am currently working on a table in HTML that displays product information. Here is the code snippet for it: <form [formGroup]="myform" (ngSubmit)="submit()" > <tbody> <tr class="group" *ngFor="let item of products;"&g ...

Is there a way to switch an element across various pages within Ionic 3?

Is it possible to exchange information between two pages using logic? I have successfully implemented a checklist, but I am unsure how to add a success/error icon next to the Room name on the 'verifyplace.html' page after invoking the goToNextPa ...

Connecting a hybrid/web client application to established remote web services outlined through a set of WSDL specifications

Summarizing the Problem I am faced with the task of integrating a mobile hybrid application, likely built on Ionic, that will need to indirectly consume several SOAP web services. My goal is for the TypeScript client in the mobile app to have knowledge of ...

How to link observables in HTTP requests using Angular 5?

I'm currently developing an application using Angular 5, and I want to segregate raw http calls into their own services so that other services can modify the responses as needed. This involves having a component, component service, and component data ...

Implementing TypeScript for augmented styling properties in a component - a guide

I have custom components defined as follows: import React from 'react'; import styled from '../../styled-components'; const StyledInput = styled.input` display: block; padding: 5px 10px; width: 50%; border: none; b ...

What is the correct way to assign a property to a function's scope?

Lately, I've been delving into Typescript Decorators to address a specific issue in my application. Using a JS bridge to communicate TS code between Android and iOS, we currently define functions as follows: index.js import foo from './bar' ...