Yovar
Yovar

Reputation: 550

generic type of child class constructor

I'm looking for a way to express the type of [B, C] from the following code in a generic way. If I hover types as it currently is I get const types: (typeof B | typeof C)[] which is a bit verbose and could get very long as new items are added.

abstract class A {
    static staticF<T extends A>(this: new () => T): string {
        return 'A'
    }
}

class B extends A {
    static bProp = 1
}
class C extends A {
    static cProp = 1
    static staticF<T extends A>(this: new () => T): string {
        return 'B'
    }
}

const types = [B, C]
types
    .map(t => t.staticF())
    .forEach(x => console.log(x))

I tried using const types: typeof A[] but I get the following error:

The 'this' context of type 'typeof A' is not assignable to method's 'this' of type 'new () => A'. Cannot assign an abstract constructor type to a non-abstract constructor type.

I also tried const types: typeof extends A[] but TS thinks I'm drunk.

How can I express the types of multiple classes constructors from classes which share the same parent?

Also, what is the difference between typeof A, new () => A and {new (): A}?

Upvotes: 1

Views: 2361

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250046

The simplest part to answer is what is the difference between typeof A, new () => A and {new (): A}. The last two are equivalent, the { new() : A } syntax is the more verbose cousin of new () => A. The reason to use the former more verbose version is because is allows you to specify more overloads for the constructor, and would also allow you to specify extra members (ie static methods). typeof A is the class A which includes the constructor signature, as well as any statics. If you just care about being able to create instances of the class, the simple constructor signature is good enough. If you need to access the statics as well you need typeof Class.

As for your other issue, the problem is that typescript treats the constructor of an abstract class as a bit of a second class constructor. It's not a constructor except inside a derived class, which, since the class should not be instantiated, this is not a bad idea. In this case however this means that when calling static on A the class A will not fulfill the constraint that it has a callable constructor (this: new () => T)

Post 4.2 Answer

You ca create a type that removes the constructor from the abstract class and then adds it back as non abstract. To remove the constructor you can just use a mapped type suck as Pick, that will copy the properties but not any function or constructor signatures from a type, and then intersect back with the constructor type:

type DerivedAClass = (new () => A) & Pick<typeof A, keyof typeof A>

let types : DerivedAClass[] = [B, C];
types
    .map(t => t.staticF()) // T of staticF will be DerivedAClass
    .forEach(x => console.log(x))

Playground Link

Pre 4.2 Answer

The simplest solution, that I found is to create an interface that extends typeof A. This will in effect erase the abstractness of the constructor and allow you to create the array you want:

type AClass = (typeof A);
interface DerivedAClass extends AClass {}

let types : DerivedAClass[] = [B, C];
types
    .map(t => t.staticF()) // T of staticF will be DerivedAClass
    .forEach(x => console.log(x))

Playground link

Upvotes: 4

Related Questions