What is the correct way to type a list of classes inheriting from the same base class?

Viewed 346

In TypeScript I'm trying to correctly type a variable of type "Array of Classes inheriting from a base class".

Using this code I get the error 'Cannot create an instance of an abstract class. ts(2511)' which I understand. But I don't want to instantiate a member of Base, just its descendants.

abstract class Base {
  abstract execute(param: string): void;
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  public commonMethod() {
    console.log("Common Method");
  }
}

class A extends Base {
  execute() {
    console.log(`Hello from A, ${this.name}`);
    this.commonMethod();
  }
}

class B extends Base {
  execute() {
    console.log(`Hello from B, no-name`);
  }
}

const list: typeof Base[] = [A, B];

const names = ["Phil", "Andy", "Bob"];

names.map((name) => list.map((cl) => new cl(name)));

How would I correctly type const list: ???[] = [A, B];

When I switch to const list: typeof A[] = [A,B] it works but then suggests all list entries are A which they are not.

Playground link

2 Answers

You can use a constructor signature instead of the class itself:


const list: Array<new (name: string) => Base> = [A, B];

const names = ["Phil", "Andy", "Bob"];

names.map((name) => list.map((cl) => new cl(name)));

Playground Link

This will not let you access statics, if you need those you could also use:

const list: Array<Omit<typeof Base, never> & (new (name: string) => Base)> = [A, B];

const names = ["Phil", "Andy", "Bob"];

names.map((name) => list.map((cl) => new cl(name)));
list[0].foo();

Playground Link

The thing is that the error is correct. The type of list is typeof Base[], and Base cannot be instantiated. As far as I know, there's no way to declare list to be "an array of non-abstract constructors deriving from Base." :-)

So as much as I dislike it, I think your best bet is to let TypeScript believe Base isn't abstract and make instantiating it directly a runtime error rather than a compile-time error by disallowing direct construction in the Base constructor:

class Base {
    // ...

    constructor(name: string) {
        if (new.target === Base) {
            throw new Error("Base cannot be directly instantiated");
        }
        this.name = name;
    }

    // ...
}

Here's a complete example (playground link):

class Base {
    protected name: string;
  
    constructor(name: string) {
        if (new.target === Base) {
            throw new Error("Base cannot be directly instantiated");
        }
        this.name = name;
    }
  
    execute(): void {
    }

    public commonMethod() {
        console.log("Common Method");
    }
}

class A extends Base {
    execute() {
        console.log(`Hello from A, ${this.name}`);
        this.commonMethod();
    }
}

class B extends Base {
    execute() {
        console.log(`Hello from B, no-name`);
    }
}

const list: typeof Base[] = [A, B];

const names = ["Phil", "Andy", "Bob"];

// This works
const instances = names.map((name) => list.map((cl) => new cl(name)));
console.log("A");
new A("a");    // So does this
console.log("B");
new B("b");    // And this
console.log("C");
new Base("c"); // But directly instantiating Base throws a runtime error
Related