javascripttypescriptconstructorthisstatic-methods

Typescript: Calling a static function that has `this` parameter


I'm currently hitting a wall in typescript.

Basically, I want to call a static function from a class extending a specific abstract class.

I get the following error

The 'this' context of type 'typeof A' is not assignable to method's 'this' of type 'AStatic<AStatic<unknown>>'. Cannot assign an abstract constructor type to a non-abstract constructor type.

Here is a link to a Typescript playground

Here is the code:

type AStatic<S extends A> = { new(): S };

abstract class A {
  static callStatic<S extends AStatic<S>>(this: AStatic<S>) {
    console.log('hey')
  }
}

class B extends A {
}


class D extends A {
}

class C {
  aType: typeof A;
  constructor(type: typeof A) {
    this.aType = type;
    this.aType.callStatic(); // error is here
  }
}

const c = new C(B);
const c_2 = new C(D);

The only way I managed to make it build in typescript is by passing any like the instead of typeof A. It's just a shame because I get no support from my IDE for the functions of A.

Note that I don't have control over class A and type AStatic, since those are from an external libray.


Solution

  • You are close! Take a look your pseudo-definition for A:

    abstract class A {
      static callStatic<S extends AStatic<S>>(this: AStatic<S>) {
      //                                      ^^^^^^^^^^^^^^^^
      //           the `this` parameter must be type AStatic<A>
    

    And looking at AStatic:

    type AStatic<S extends A> = { new(): A };
    //                            ^^^^^ - type must have a constructor, 
    //                                      which rules out abstract classes.
    

    This rules out typeof A as the this parameter, since it is abstract. We could try using AStatic directly:

    class C {
      aType: AStatic<A>;
      constructor(type: AStatic<A>) {
        this.aType = type;
        this.aType.callStatic();
        //         ^^^^^^^^^^ - Property 'callStatic' does not exist on type 'AStatic<A>'
      }
    }
    

    But callStatic is not defined on AStatic. The solution is an intersection type:

    class C {
      aType: AStatic<A> & typeof A
      constructor(type: AStatic<A> & typeof A) {
        this.aType = type;
        this.aType.callStatic() // works!
      }
    }
    

    As MunsMan pointed out though, unless you override callStatic on your derived types you don't need to pass typeof A at all:

    const c = new C(B);
    c.callStatic(); // hey
    B.callStatic(); // hey
    D.callStatic(); // hey
    

    In other words, as long as you have a non-abstract version of A, you can call callStatic (or any static method/property) on that type and it will work the same way every time!