Signature for generic function that returns an instance of the (possibly abstract) class it's passed

Viewed 46

Basically, I'm trying to type getA() in the following (drastically simplified) code:

interface Base {
  readonly id: string
}

class Foo implements Base {
    get id() {return 'a foo'}
    get ping() {return 'pong'}
}

abstract class Bee {
    abstract get bing(): string
}

class Bar implements Base, Bee { 
    get id() {return 'a bar'}
    get bing() {return 'Crosby'}
}

class Baz implements Base, Bee {    
    get id() {return 'a bar'}
    get bing() {return 'cherry'}
}

function getA(type, name) {
    // Some dynamic lookup happens here
    if (type === Foo) {
        return new Foo()
    } else if (type === Bee && name === 'bar') {
        return new Bar()
    } else if (type === Bee && name === 'baz') {
        return new Baz()
    } else {
        throw new Error('unknown args')
    }
}

console.log(getA(Foo, 'foo').ping)
console.log(getA(Bee, 'bar').bing)

This is doable (though awkward) without the abstract classes:

function getA<T extends Base>(type: {new(): T}, name: string): T

but abstract classes don’t have constructors (for obvious reasons) and I can’t seem to find any other way to refer to the type (rather than an instance of it). I’m also open to suggestions of possible refactors if there might be a better way of doing this.

1 Answers

You can use an abstract constructor type expression which looks just like a normal "concrete" constructor type expression*, except you write abstract new instead of just new:

interface Foo {
  a: string;
}
type FooCtor = new (a: string) => Foo;
type AbstractFooCtor = abstract new (a: string) => Foo;

If you have a concrete constructor you can new it:

declare let ConcreteFoo: FooCtor;
new ConcreteFoo("yes"); // okay
   

But you can't do that with an abstract constructor:

let AbstractFoo: AbstractFooCtor;
new AbstractFoo("no"); // error, cannot create an instance of an abstract class
    

Note that abstract constructors are considered a proper supertype of concrete constructors, so you can use a concrete constructor in place of an abstract one, but not vice versa:

AbstractFoo = ConcreteFoo; // okay, concrete ctor is a subtype of abstract ctor
ConcreteFoo = AbstractFoo; // error, abstract ctor is not a subtype of concrete ctor

Anyway, that means you can write getA()'s call signature like

function getA(type: abstract new () => any, name: string) {/*...*/}
// or whatever arg/ret you want  ^^^^^^^^^

and everything should work.

Playground link to code

*The TS documentation refers to construct signatures and abstract construct signatures instead of constructor type expressions, but this is a bit ambiguous or incorrect. A call/construct signature looks like { (arg: Arg): Ret } or { new (arg: Arg): Ret } whereas a function/constructor type expression looks like (arg: Arg) => Ret or new (arg: Arg) => Ret. This difference matters here, because while abstract new (arg: Arg) => Ret is valid TypeScript syntax since TS4.2, the potentially equivalent { abstract new (arg: Arg): Ret } is invalid syntax. Hence my terminology above.

Related