Home > Software engineering >  how to get params of generic constructor
how to get params of generic constructor

Time:02-18

I have a class Collection which accepts another class as param personClass. I suppose method add should accepts params which includes in constructor of 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'.

Then I will try to make personClass as optional param. By default personClass should be equals Person

How to avoid error in this method collection.add('123')

CodePudding user response:

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

CodePudding user response:

@jcalz proposed solution and it works

  • Related