abstract new (constructor types)

Types a constructor that may be abstract, so mixins and factories can accept an abstract class.

Signature
type AbstractConstructor<T> = abstract new (...args: any[]) => T;

// equivalent object-type form
type AbstractCtor<T> = { abstract new (...args: any[]): T };

// contrast: only concrete classes are assignable here
type Constructor<T> = new (...args: any[]) => T;
  • Added in TypeScript 4.2

Parameters

ParameterTypeDescription
$...argsany[]The constructor's parameter list. Mixin factories almost always use a rest parameter of any[], because the base class's constructor arity is unknown and a narrower list would reject most classes.
$TobjectThe instance type produced. Constrain it (for example `T extends object`) when the value is used as a base class in an `extends` clause.

Return value

TThe instance type the constructor produces. The signature describes the shape of `new Ctor(...)` even when the class itself cannot be instantiated directly.

Errors

Assigning an abstract class to a plain construct signature is an error: "Cannot assign an abstract constructor type to a non-abstract constructor type." Calling `new` on a value typed as an abstract construct signature is also an error, since it may be abstract.

Changelog

  • PHP 4.2Construct signatures may be marked abstract, so abstract classes become assignable to a constructor type and can be passed to mixin factories.

A construct signature such as new () => Base promises the value can be instantiated. An abstract class cannot, so passing one produces the familiar error: Cannot assign an abstract constructor type to a non-abstract constructor type. Marking the signature abstract drops that promise and accepts both abstract and concrete classes.

When to reach for it

  • Mixin factories. function withStyles<T extends AbstractConstructor<object>>(Ctor: T) accepts an abstract base and returns a subclass, which is exactly the intent: extend it, never instantiate it.
  • Any helper that takes a class as a value only to read its statics, subclass it, or use it in an instanceof check.

What trips people up

The modifier removes the ability to call new on that parameter — that is the whole trade. A factory that both accepts abstract classes and instantiates them is not expressible, and it should not be: instantiating an abstract class would leave its abstract members undefined. If your factory really does construct instances, keep the non-abstract signature and require concrete classes. Also note the rest parameter: writing abstract new () => T with an empty list rejects almost every real base class, since constructor parameters are checked. Use ...args: any[] for mixins.

Example

Example · typescript
type AbstractConstructor<T> = abstract new (...args: any[]) => T;

abstract class Shape {
  abstract area(): number;
  summary(): string { return `area=${this.area()}`; }
}

function withLabel<T extends AbstractConstructor<Shape>>(Base: T) {
  abstract class Labelled extends Base {
    label = "shape";
    describe(): string { return `${this.label}: ${this.summary()}`; }
  }
  return Labelled;
}

class Square extends withLabel(Shape) {
  constructor(private side: number) { super(); }
  area(): number { return this.side ** 2; }
}

console.log(new Square(4).describe());

When to use it

  • Write a mixin factory that can extend an abstract base class as well as a concrete one.
  • Accept any class in a helper that only reads its static members or uses it with instanceof.
  • Type a registry of class references where some entries are abstract intermediate classes.

Frequently asked questions

What does 'Cannot assign an abstract constructor type to a non-abstract constructor type' mean?

It means you passed an abstract class where the parameter type is `new (...) => T`, which promises the value can be instantiated. Change the parameter's type to `abstract new (...) => T` if you only extend the class, or require a concrete subclass if you actually call new on it.

How do I write a mixin that works with abstract classes?

Constrain the factory's type parameter to `abstract new (...args: any[]) => T` instead of `new (...args: any[]) => T`. Use a rest parameter of any[], because a fixed parameter list rejects base classes whose constructors take different arguments.

Can I call new on an abstract construct signature?

No — dropping the instantiability promise is exactly what the modifier does, so `new Ctor()` on such a value is an error. A factory that constructs instances must keep the plain `new (...) => T` signature and accept only concrete classes.

More examples

The error the modifier removes

Prints "12.566" then "function function". The assignment that fails and the one that succeeds differ only by the modifier — assignability is the entire feature. Note the last binding: a concrete class is assignable to an abstract construct signature too, so the abstract form is the wider type, not a separate one.

Example · typescript
abstract class Shape {
  abstract getArea(): number;
}

interface HasArea {
  getArea(): number;
}

// Rejected: the signature promises `new Ctor()` works, and it does not.
// let Concrete: new () => HasArea = Shape;
// Error: Cannot assign an abstract constructor type to a non-abstract
// constructor type.

// Accepted: the signature makes no such promise.
let Abstract: abstract new () => HasArea = Shape;

class Circle extends Shape {
  constructor(private r: number) { super(); }
  getArea(): number { return Math.PI * this.r ** 2; }
}

// Concrete classes satisfy both forms.
let Either: abstract new (r: number) => HasArea = Circle;
console.log(new Circle(2).getArea().toFixed(3));
console.log(typeof Abstract, typeof Either);

Why you cannot call new on it

Prints "[timed] report" and [ 'printed', 'printed' ]. The two factories show the trade precisely: timed accepts an abstract base but may only extend it, while createAll calls new and therefore has to keep the non-abstract signature — which is what rejects Task at the last line.

Example · typescript
type AbstractConstructor<T> = abstract new (...args: any[]) => T;
type Constructor<T> = new (...args: any[]) => T;

abstract class Task {
  abstract run(): string;
}
class PrintTask extends Task {
  run(): string { return "printed"; }
}

// Extends only — accepts abstract bases.
function timed<T extends AbstractConstructor<Task>>(Base: T) {
  abstract class Timed extends Base {
    runTimed(): string { return `[timed] ${this.run()}`; }
  }
  return Timed;
  // new Base();
  // Error: Cannot create an instance of an abstract class.
}

// Instantiates — must demand a concrete class.
function createAll<T extends Constructor<Task>>(Ctor: T, n: number): Task[] {
  return Array.from({ length: n }, () => new Ctor());
}

class Report extends timed(Task) {
  run(): string { return "report"; }
}

console.log(new Report().runTimed());
console.log(createAll(PrintTask, 2).map((t) => t.run()));
// createAll(Task, 2);  Error: abstract constructor type not assignable.

Discussion

  • Be the first to comment on this lesson.