6

I have an abstract class, and classes that extend it in an array. What do I type the array?

abstract class AbstractClassToExtend {
    constructor() { console.log("hello") }
}

class One extends AbstractClassToExtend {}
class Two extends AbstractClassToExtend {}

const array = [One, Two] // what do i type this array?

I've tried const array: typeof AbstractClassToExtend[] = [One, Two], but when making an instance of one of the classes in the array,

new array[0]()

it gives an error:

error TS2511: Cannot create an instance of an abstract class.

I'm using Typescript 4.3.2.

hf02
  • 171
  • 2
  • 11
  • Did you mean to write `const array: typeof AbstractClassToExtend[] = [One, Two]`? Also can you include the code where you are trying to instantiate these classes that is giving you that error? – Alex Wayne Jun 01 '21 at 23:29

4 Answers4

3

You specifically want to instantiate the types in the array, so I'm not sure you can type the array as having abstract classes.

Perhaps this is not as elegant as you hoped, but you could have a concrete base implementation from which the others inherit:

abstract class AbstractClassToExtend { }

class BaseImplementation extends AbstractClassToExtend { }

class One extends BaseImplementation { }
class Two extends BaseImplementation { }

const array: (typeof BaseImplementation)[] = [One, Two]

const a: BaseImplementation = new array[0]();
const b: BaseImplementation = new array[1]();

An alternative is to have an array of functions that return an instance of your abstract type:

const functions: Array<() => AbstractClassToExtend> = [() => new One(), () => new Two()];

const c: AbstractClassToExtend = functions[0]();
const d: AbstractClassToExtend = functions[1]();
Frank Modica
  • 10,238
  • 3
  • 23
  • 39
  • The typing of the array only requires that its elements are classes with a zero-argument constructor, but not that they extend `BaseImplementation` (I can add to the array classes such as String and Number, whose arguments are optional). – astroide Jun 02 '21 at 11:15
  • @astroide Is that a problem with my example, or just how TypeScript works with duck typing? – Frank Modica Jun 14 '21 at 00:22
  • Yes, TypeScript is using some kind of duck typing. More specifically, if `Duck` is a class with a `quack()` method returning a `string` & you have a variable of type `Duck`, you can assign that variable any object that has the same properties and methods, e.g. `{"quack":()=>"quack"}`. Same thing if you have a type `type DuckLike = {quack: ()=>string}`, you can assign any variable of type `DuckLike` a `Duck` instance, because it shares exactly the same properties & methods. – astroide Jun 14 '21 at 00:56
  • see this question https://stackoverflow.com/questions/48829743/why-duck-typing-is-allowed-for-classes-in-typescript – astroide Jun 14 '21 at 22:00
1

You can use a union type:

const array: (One | Two)[] = [One, Two]

More here: Defining array with multiple types in TypeScript

Seth Lutske
  • 9,154
  • 5
  • 29
  • 78
0

I got this working in a slightly different way that the accepted answer and it may suit some use cases where creating a BaseImplementation is not ideal.

Instead of using the class constructor directly, add a generic static method to initialize the abstract class. This allows you to have a this type that is initializable.

abstract class AbstractClassToExtend {
  constructor() {
    console.log("hello")
  }

  static init<T extends AbstractClassToExtend >(this: new () => T) {
      return new this()
  }
}

Then add new () => AbstractClassToExtend to the type of the array's items. This makes the array items compatible with calling AbstractClassToExtend.init().

const array: (typeof AbstractClassToExtend & (new () => AbstractClassToExtend))[] = [One, Two]

Use the init method to create an instance instead of new array[0]():

const array: (typeof AbstractClassToExtend & (new () => AbstractClassToExtend))[] = [One, Two]
const one: AbstractClassToExtend = array[0].init()
const two: AbstractClassToExtend = array[1].init()
0

If this array will not be changing at runtime, and if you are okay with a literal inference, as const could be a more fluid solution (available since 3.4.x).

Example:

class One extends AbstractClassToExtend {}
// Adding a simple property so that the classes are different
class Two extends AbstractClassToExtend {
  foo = "foo";
}

const array = [One, Two] as const;

const instanceOfOne = new array[0](); // typed as `One`
const instanceOfTwo = new array[1](); // typed as `Two`

// If you need a more concrete type to reference in other parts
// of your application.
type ConstructorArray = typeof array;


// Will error _only_ if the types are sufficiently different
// In these two examples, `One` does not have all the required properties of `Two`
const incorrectArrayA: ConstructorArray = [Two, One]; 
const incorrectArrayB: ConstructorArray = [One, One];

// An exact match works as expected
const correctArrayA: ConstructorArray = [One, Two];

// A more interesting example is when we extend further
class Even extends Two {
  bar = 'bar';
}

const correctArrayB: ConstructorArray = [One, Even];
const correctArrayC: ConstructorArray = [Two, Two];
const correctArrayD: ConstructorArray = [Even, Even];

Overall:

  • This approach is respectably terse, and it is more strict than a union type.
  • It does not require a utility class that extends your abstract class just for the sake of typing, which can be especially useful when you need to have varying properties and methods between classes.
  • It does not depend on static methods or on pre-baked constructor calls, which can be challenging to handle if the signature of the constructor is different between the extending classes.
Dallas
  • 68
  • 1
  • 6