Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript Class as parameter that extends another class

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?

like image 380
Daniel Cruz Avatar asked May 30 '26 10:05

Daniel Cruz


1 Answers

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:

  • it creates an instance of B, not A
  • it has a return type of [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:

  • static method works for A.generate3(), and likewise for subclasses of A
  • return type is correct
  • return value is correct

It has the weird side effect of having to new up a bonus object and throw it away.

like image 140
alexanderbird Avatar answered Jun 02 '26 02:06

alexanderbird



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!