4

I'm writing some graphing code in TypeScript 2.6, and I'm have a small obstacle with respect to inheritance and generic code:

abstract class Series { /**/ }

class LineSeries    extends Series { /**/ }
class BarSeries     extends Series { /**/ }
class ScatterSeries extends Series { /**/ }

class Chart {
  private seriesArray: Series[] = [];

  constructor(series: Series[]) {
    let seriesType: typeof Series = null;

    switch (series.SeriesType) {
      case SeriesType.LineSeries:
        seriesType = LineSeries;
        break;
      case SeriesType.BarSeries:
        seriesType = BarSeries;
        break;
      case SeriesType.ScatterSeries:
        seriesType = ScatterSeries;
        break;
      default:
        throw "Unsupported series type";
    }

    for (let i = 0; i < series.length; i++)
      this.SeriesArray.push(new seriesType());
  }
}

I intended the code above to construct a new object that is derived from Series, where the constructor on the last line comes from a runtime-assigned type value. The problem I have is that Series is abstract, so I cannot instantiate it, however I will never assign the value typeof Series to the series variable. (But TypeScript doesn't know that).

I have thought of a few solutions, but none of them are especially elegant. One is to simply remove the abstract modifier from Series and optionally put in some code to throw an exception if it's instantiated. Another solution is to change the definition of seriesType to:

let seriesType: (typeof LineSeries | typeof BarSeries | typeof ScatterSeries) = null;

but then I need to maintain this list every time I add a new type of series. I know I can do, for example:

type DerivedSeries = (typeof LineSeries | typeof BarSeries | typeof ScatterSeries);

and then

let seriesType: DerivedSeries = null;

but this is not much of an improvement, if at all.

Is there any more elegant way I can tell TypeScript that seriesType is of type Series, but must also be of an instantiable child type? i.e. the expression seriesType = Series; should throw a compile-time error, not new seriesType();.

Thanks

Ozzah
  • 10,631
  • 16
  • 77
  • 116

1 Answers1

6

The simplest way to instantiate classes derived from an abstract class is by defining an object type with the new() method:

abstract class AbstractClass { /**/ }

class DerivedClassA extends AbstractClass { /**/ }
class DerivedClassB extends AbstractClass { /**/ }

const classCtors: Array<{ new(): AbstractClass }> = [
    DerivedClassA,
    DerivedClassB
];

for (const ctor of classCtors) {
    const instance = new ctor();
}
Grinde
  • 326
  • 1
  • 11
  • In fact, it's sufficient to replace `let seriesType: typeof Series = null;` in original question code with `let seriesType: {new(): Series} = null;` – artem Jan 18 '18 at 02:04
  • I don't really understand what's going on here except that it seems to be an anonymous object whose ctor returns an `AbstractClass` (or `Series`) object. In testing, it works, except my statements in the `switch`, such as `seriesType = LineSeries;`, don't work: Type 'typeof LineSeries' is not assignable to type 'new () => Series'. Also it's complaining about the number of arguments I left out of my original question, but presumably I need to add these inside the `new()`. – Ozzah Jan 18 '18 at 02:33
  • 1
    @artem - I was hesitant to use the original question code because it's got quite a few errors. For example the `for` loop is floating in the class body, and the switch is operating on a non-existent property. I assume it was just copied from a few different places. – Grinde Jan 18 '18 at 02:35
  • @Ozzah - Yes you'd have to add the arguments to the `new()` method (or use `new(...args: any[])`, though I wouldn't recommend it). It's just a normal method definition, but for using `new` on the object. It lets you define the arguments and return type as you would with any other method. If your classes have static methods, or methods not defined on the abstract base class that will complicate things. – Grinde Jan 18 '18 at 02:40
  • @Grinde sorry - I just typed it for the question. – Ozzah Jan 18 '18 at 02:46
  • 1
    @Ozzah that error - `Type 'typeof LineSeries' is not assignable to type 'new () => Series'` - can happen if `LineSeries` class has constructor that takes some arguments, so it's incompatible with [constructor signature](https://stackoverflow.com/questions/13407036/how-does-typescript-interfaces-with-construct-signatures-work) without any arguments. That anonymous object type `{new(....): Series}` must declare arguments for its `new()` so that it's compatible with all derived class constructors. – artem Jan 18 '18 at 02:54
  • @artem Got it. That error totally didn't lead me in the right direction. Thanks! – Ozzah Jan 18 '18 at 02:56