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
.
CodePudding user response:
@jcalz proposed solution and it works