glm9637
glm9637

Reputation: 894

Passing abstract class as parameter in Typescript

I am trying to set up a class that dynamically builds a filter string. To do this, i created the following class:

export class UrlBuilder<T extends BaseSpalten> {

    private spalten: T;

    ....

    public filterData(config: (spalten: T, addParameter: (key: string, value: string) => void,
        addParameterListe: (key: string, value: string[]) => void) => void): UrlBuilder<T> {
        config(this.spalten, this.addQueryParameter, this.addQueryParamterList);
        return this;
    }

    ....
}

But now i have the Problem that the class "BaseSpalten" and the extended classes are static classes:

export class BaseSpalten {
    public static AenderungVersion: string = 'AenderungVersion';
    public static Dirty: string = 'Dirty';
}

But i need the properties of the classes inside the config function, to ensure i get a valid filter. How would i pass a reference to the static T to the config function?

Here is a sample Playground link for better understanding

Upvotes: 1

Views: 4685

Answers (1)

jcalz
jcalz

Reputation: 330286

As far as I can tell, we can reduce your question to the following. Given a class hierarchy with some static properties:

class BaseClass {
    public static staticPropBase: string = 'foo';
}

class SubClass extends BaseClass {
    public static staticPropSub: string = 'bar';
}

How can we access those static properties from instances of the class? At runtime this is actually straightforward; all JavaScript objects have a constructor property that refers to the constructor function that created it. So (new SubClass()).constructor will be SubClass, with all its static properties.

So you can write this and it will work:

const subclass = new SubClass();
console.log(subclass.constructor.staticPropBase); // TS error, but "foo" at runtime
console.log(subclass.constructor.staticPropSub);  // TS error, but "bar" at rntime

But the problem is that the TypeScript compiler is unhappy. It does not realize that subclass.constructor is the SubClass constructor. All it knows about subclass.constructor is that it's a Function. That's true (class constructors are functions) but not specific enough to be helpful.

But wait, shouldn't the TypeScript compiler know that the type of (new SubClass()).constructor is typeof SubClass itself? Yes, probably... and it's a longstanding open issue, see microsoft/TypeScript#3841. The stated reason why this is not done automatically is that subclass constructors are not required to be valid subtypes of their superclass constructors. In the following scenario:

class A {
    a: string = "a";
    constructor() { } // no param
}

class B extends A {
    b: number;
    constructor(b: number) { // one param 
        super();
        this.b = b;
    }
}

even though the instance type B is a subtype of the instance type A, its constructor typeof B is not a subtype of typeof A (because B's constructor requires an argument, while A's does not):

const a: A = new B(123); // okay
const _A: typeof A = B; // error! Type 'typeof B' is not assignable to type 'typeof A'.(2322)

If the TypeScript compiler automatically gave A a constructor property of type typeof A and B a constructor property of type typeof B, then the instance types B would no longer be a valid subtype of A, and the extends model of subtyping would fail.

Blecch. It would definitely be nice if TS could do something better than Function, though. But even if they eventually make a change, you have your problem today. What can you do now?


Probably the best current workaround is to manually declare that your class instances have a constructor property of the right type. Since your class constructors are empty, typeof SubClass is a valid subtype of typeof BaseClass, so the above problem doesn't come up. (If it does come up, you could probably use a type like Pick<typeof SubClass, keyof typeof SubClass> to keep the static properties but ignore the constructor signature. More info available upon request.)

The declaration is a little weird, but it looks like this:

class BaseClass {
    declare ["constructor"]: typeof BaseClass; // TS 3.7+
    //["constructor"]!: typeof BaseClass; // TS 3.6-
    public static staticPropBase: string = 'foo';
}

class SubClass extends BaseClass {
    declare ["constructor"]: typeof SubClass; // TS 3.7+
    //["constructor"]!: typeof SubClass; // TS 3.6-
    public static staticPropSub: string = 'bar';
}

(Note that for TS3.7 and above you should use a declare property modifier, whereas for TS2.7 through TS3.6 you should use a definite assignment assertion. They basically do the same thing. More info on request.)

And now the following just works:

const subclass = new SubClass();
console.log(subclass.constructor.staticPropBase); // okay, "foo"
console.log(subclass.constructor.staticPropSub);  // okay, "bar"

Otherwise you can always just use type assertions to suppress the compiler's warnings:

console.log((subclass.constructor as typeof SubClass).staticPropBase); // okay, "foo"
console.log((subclass.constructor as typeof SubClass).staticPropSub);  // okay,  "bar" 

Personally I'd rather strongly type the class definition, but type assertions will also work.


Okay, hope that helps; good luck!

Playground link to code

Upvotes: 3

Related Questions