I'm fairly new to Typescript and generics; I must be missing something hopefully trivial.
I'm trying to pass a (generic) class as an argument for a function, but this class extends from another specific class
An oversimplified example would be the following: lets say I have
class A {
static generate3() { return [new A(),new A(),new A()]; }
}
class B extends A {}
class C extends A {}
I want a method that I would call with any of the classes that inherite from A as parameters and return the result of that static method. Something like
f(B) // returns type B[]
I figured I can do
function f(type: typeof B){
return type.generate3();
}
But this requires me to define the class in advance. I also cannot use typeof B|typeof C cause in real life there is too many clases for this to be practical I tried
function f2<T>(type: typeof T extends A){
return type.generate3();
}
where T is supposed to be the class, but it throws the following error: 'T' only refers to a type, but is being used as a value here.
I figured this works
function f3(type: typeof A){
return type.generate3();
}
But the return type of f3(B) is still A[] instead of the desired B[] I tried mixing the two like:
function f4<T extends A>(type: typeof A) : T[]{
return type.generate3() as T[]; // cast it
}
But the return type of f4(B) is still A[]
I don't understand it. Can anyone figure out what I'm I doing wrong?
In your example, all of A, B, and C have a static method generate3() { return [new A(),new A(),new A()]; }. TypeScript is correctly telling you that whether you call A.generate3(), B.generate3(), or C.generate3(), all three of those will execute the JavaScript return [new A(),new A(),new A()] and return you three As.
If you want generate3 to return something other than A, then generate3 must not directly refer to A.
class A {
name = 'instance of A';
static generate3() { return [new A(),new A(),new A()]; }
}
class B extends A {
name = 'instance of B';
}
class C extends A {
name = 'instance of C';
}
console.log(`A: ${A.generate3().map(x => x.name)}`);
console.log(`B: ${B.generate3().map(x => x.name)}`);
console.log(`C: ${C.generate3().map(x => x.name)}`);
All three of these log "instance of A" three times.
To write a function generate3 that accepts a class Type extends A and returns three instances of Type, you could do the following:
class A {
name = 'instance of A';
}
class B extends A {
name = 'instance of B';
}
class C extends A {
name = 'instance of C';
}
interface Constructor<Type extends A> {
new (): Type;
}
function generate3<Type extends A>(constructor: Constructor<Type>): [Type, Type, Type] {
return [new constructor(), new constructor(), new constructor()];
}
console.log(`A: ${generate3(A).map(x => x.name)}`);
console.log(`B: ${generate3(B).map(x => x.name)}`);
console.log(`C: ${generate3(C).map(x => x.name)}`);
(Run in TypeScript playground)
This prints:
"A: instance of A,instance of A,instance of A"
"B: instance of B,instance of B,instance of B"
"C: instance of C,instance of C,instance of C"
The types are as expected:
const c: C = generate3(C)[1] // OK
const b: B = generate3(C)[1] // 'C' is not assignable to type 'B'
That doesn't give you a static generate3 method on the classes, though. You asked for B.generate3(), this option is generate3(B).
One further iteration, which I think is theoretically interesting but probably too weird to use in production (unless you have a really good reason).
class A {
name = 'instance of A';
generate3(): [typeof this, typeof this, typeof this] {
const ThisClass: Constructor<typeof this> = this.constructor as any;
return [new ThisClass(), new ThisClass(), new ThisClass()];
}
}
This generate3 can be used like so: new B().generate3() and has the following characteristics:
B, not A[B, B, B]I don't know what implications there are for using the .constructor property and newing it up. TypeScript didn't accept it until I used as any, which reinforces my sense that this is not a great approach.
We can go even further, well into the territory of "just because you can doesn't mean you should", to make this static:
class A {
private generate3(): [typeof this, typeof this, typeof this] {
const ThisClass: Constructor<typeof this> = this.constructor as any;
return [new ThisClass(), new ThisClass(), new ThisClass()];
}
static generate3() {
return new this().generate3();
}
}
This now has all the desired characteristics:
A.generate3(), and likewise for subclasses of AIt has the weird side effect of having to new up a bonus object and throw it away.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With