Abstract Construct Signatures in TypeScript erklärt
AI generated
type
TypeScript · Generics
Abstract Construct Signatures: abstrakte Klassen typsicher als Parameter
Der Typ abstract new (...) => T seit TypeScript 4.2

Wer eine Factory Funktion, einen Mixin oder einen kleinen Dependency Injection Container baut, stößt schnell auf ein Problem: Der Typ new (...) => T akzeptiert nur konkrete, instanziierbare Klassen. Abstrakte Basisklassen fallen durch das Raster, obwohl man sie oft genau dort braucht, wo man Subklassen generisch verarbeiten will. Genau dafür gibt es seit TypeScript 4.2 die abstract construct signature.

10 Min. Lesezeit TypeScript 4.2+ Generics Factory Pattern

1. Das Problem: new (...) => T scheitert bei abstrakten Klassen

Der klassische Konstruktortyp new (...args: any[]) => T beschreibt etwas, das man mit new aufrufen kann. Genau darin liegt die Einschränkung: TypeScript prüft an dieser Stelle, ob der Typ tatsächlich instanziierbar ist. Eine abstrakte Klasse ist es per Definition nicht, denn sie kann fehlende Implementierungen enthalten und darf zur Laufzeit nicht direkt mit new erzeugt werden.

Das führt zu einem konkreten Compilerfehler, sobald man versucht, eine abstrakte Klasse als Wert eines Parameters vom Typ new (...) => T zu übergeben. Die Fehlermeldung lautet sinngemäß, dass sich der abstrakte Konstruktortyp nicht dem konkreten Konstruktortyp zuweisen lässt. Vor TypeScript 4.2 musste man diese Prüfung oft mit any oder unsauberen Type Assertions umgehen, was die eigentliche Absicht des Codes verschleiert hat.

Dabei ist der Wunsch legitim: Eine Funktion soll eine Basisklasse als Blaupause entgegennehmen, ohne selbst zu wissen oder wissen zu müssen, ob diese Basisklasse abstrakt ist oder nicht. Erst die abstract construct signature erlaubt es, diese Unterscheidung im Typsystem sauber abzubilden, statt sie stillschweigend zu ignorieren.


abstract class Repository {
  abstract findAll(): unknown[];
}

function createInstance<T>(ctor: new () => T): T {
  return new ctor();
}

// Fehler: Der abstrakte Konstruktortyp "typeof Repository"
// ist dem konkreten Konstruktortyp "new () => T" nicht zuweisbar.
createInstance(Repository);

2. Grundsyntax: der Typ abstract new (...) => T

Seit TypeScript 4.2 kann man dem Schlüsselwort new in einem Funktionstyp das Modifikatorwort abstract voranstellen: abstract new (...args: any[]) => T. Dieser Typ beschreibt einen Konstruktor, der sowohl von einer konkreten als auch von einer abstrakten Klasse stammen darf. Er ist also strikt weiter gefasst als new (...args: any[]) => T.

Wichtig ist die Richtung der Zuweisbarkeit: Jede konkrete Klasse lässt sich problemlos einer Variable vom Typ abstract new (...) => T zuweisen, denn eine instanziierbare Klasse erfüllt automatisch die schwächeren Anforderungen des abstrakten Konstruktortyps. Umgekehrt funktioniert es nicht: Ein Wert vom Typ abstract new (...) => T lässt sich nicht dem konkreten Typ new (...) => T zuweisen, weil der Compiler nicht garantieren kann, dass sich dahinter tatsächlich eine instanziierbare Klasse verbirgt.

Diese Asymmetrie ist beabsichtigt und spiegelt exakt die Laufzeitsemantik wider: Man darf mit weniger Information mehr Typen annehmen, aber man darf aus einem allgemeineren Typ nicht automatisch auf konkrete Instanziierbarkeit schließen.


abstract class Animal {
  abstract makeSound(): string;
}

class Dog extends Animal {
  makeSound(): string {
    return "Wuff";
  }
}

let ctor: abstract new () => Animal;

ctor = Animal; // erlaubt: abstrakte Klasse
ctor = Dog;    // erlaubt: konkrete Klasse ist ebenfalls kompatibel

let concreteCtor: new () => Animal;
// concreteCtor = ctor; // Fehler: abstract new ist keinem new zuweisbar
concreteCtor = Dog;     // erlaubt: Dog ist konkret

3. Praktisches Beispiel: eine Factory-Funktion für Subklassen

Der klassische Anwendungsfall ist eine Factory Funktion, die eine Basisklasse als Konfigurationsparameter entgegennimmt, aber selbst niemals versucht, diese Basisklasse direkt zu instanziieren. Stattdessen erwartet die Funktion, dass am Ende eine konkrete Subklasse übergeben wird, oder sie kombiniert die Basisklasse mit weiterer Logik, bevor sie eine konkrete Klasse erzeugt.

Im folgenden Beispiel nimmt createRepository einen Konstruktor vom Typ abstract new () => Repository entgegen. Innerhalb der Funktion wird nicht die abstrakte Basisklasse instanziiert, sondern nur der übergebene, konkrete Konstruktor, der zur Laufzeit garantiert eine Subklasse von Repository ist. Der Rückgabetyp bleibt an die Basisklasse gebunden, was für den Aufrufer ausreicht, weil er ohnehin nur gegen die abstrakte Schnittstelle programmiert.

Diese Konstruktion ist besonders in Dependency Injection Containern nützlich: Der Container kennt zur Registrierungszeit nur die abstrakte Basisklasse als Vertrag, erhält zur Laufzeit aber eine konkrete Implementierung, die er ohne weitere Type Assertions instanziieren kann.


abstract class Repository {
  abstract findAll(): unknown[];
}

class InMemoryRepository extends Repository {
  private items: unknown[] = [];

  findAll(): unknown[] {
    return this.items;
  }
}

function createRepository<T extends Repository>(
  ctor: abstract new () => T,
  concreteCtor: new () => T,
): T {
  // ctor dient nur als Vertrag/Dokumentation, instanziiert wird
  // ausschließlich der garantiert konkrete Konstruktor.
  return new concreteCtor();
}

const repo = createRepository(Repository, InMemoryRepository);
console.log(repo.findAll());

4. Generische Constraints mit abstract new (...args: any[]) => any

In der Praxis begegnet man abstract new meist nicht als konkreter Parametertyp, sondern als Constraint eines generischen Typparameters: <T extends abstract new (...args: any[]) => any>. Diese Formulierung akzeptiert jeden Konstruktor, egal ob abstrakt oder konkret, egal wie viele Argumente er erwartet und egal welchen Instanztyp er erzeugt.

Der Vorteil dieser weiten Constraint liegt darin, dass generische Hilfsfunktionen sich nicht künstlich auf konkrete Klassen beschränken müssen, wenn sie eigentlich nur an der Struktur der erzeugten Instanz interessiert sind. Mit InstanceType<T> lässt sich aus dem Konstruktortyp anschließend der konkrete Instanztyp extrahieren, unabhängig davon, ob T abstrakt oder konkret war.

Diese Kombination aus abstraktem Konstruktortyp als Constraint und InstanceType als Extraktionswerkzeug ist die Grundlage vieler generischer Utility Funktionen in größeren TypeScript Codebasen, etwa für Serialisierung, Validierung oder Registrierungsmechanismen.


type AnyConstructor = abstract new (...args: any[]) => any;

function getClassName<T extends AnyConstructor>(ctor: T): string {
  return ctor.name;
}

abstract class Shape {
  abstract area(): number;
}

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

getClassName(Shape);  // "Shape", funktioniert auch für abstrakte Klassen
getClassName(Circle); // "Circle"

type CircleInstance = InstanceType<typeof Circle>; // Circle

5. Vergleich zu Mixin-Funktionen

Klassische Mixin Funktionen in TypeScript arbeiten mit dem Typ new (...args: any[]) => {}. Dieser Typ verlangt explizit einen konkreten, instanziierbaren Konstruktor, weil eine Mixin Funktion typischerweise eine neue Klasse erzeugt, die von der übergebenen Basisklasse erbt und diese direkt aufruft: class Mixed extends Base { ... }. Genau an dieser Stelle würde eine abstrakte Basisklasse zwar syntaktisch als Erweiterung funktionieren, aber die resultierende Klasse müsste dann selbst wieder alle abstrakten Member implementieren.

Der Unterschied zur Factory Funktion aus dem vorherigen Abschnitt liegt in der Verwendung des Konstruktors: Ein Mixin erweitert die Basisklasse per extends und erzeugt daraus eine neue Klasse, während eine Factory Funktion typischerweise direkt eine Instanz per new erzeugt. Erweiterung per extends funktioniert auch mit abstrakten Klassen als Ausgangspunkt, solange am Ende eine konkrete, vollständig implementierte Klasse entsteht.

Manche fortgeschrittenen Mixin Bibliotheken kombinieren deshalb beide Konzepte: Sie akzeptieren als Constraint abstract new (...args: any[]) => any, um auch abstrakte Basisklassen als Ausgangspunkt für eine Erweiterung zuzulassen, erzeugen aber selbst nur konkrete, vollständig implementierte Klassen als Ergebnis.


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

function Timestamped<TBase extends AbstractConstructor>(Base: TBase) {
  abstract class TimestampedClass extends Base {
    createdAt = new Date();
  }
  return TimestampedClass;
}

abstract class Entity {
  abstract id: string;
}

// Mixin auf Basis einer abstrakten Klasse, Ergebnis bleibt abstrakt,
// solange "id" nicht implementiert wird.
class TimestampedEntity extends Timestamped(Entity) {
  id = "entity-1";
}

6. Zusammenspiel mit statischen Methoden und Membern

Abstrakte Klassen können seit TypeScript 4.2 auch abstrakte statische Member deklarieren, die dem Konstruktortyp selbst zugeordnet sind. Wenn eine Factory Funktion einen Konstruktor vom Typ abstract new () => T entgegennimmt, kann sie dennoch auf statische Methoden und Properties dieses Konstruktors zugreifen, sofern sie im Typ deklariert wurden. Der Zugriff funktioniert identisch zu konkreten Konstruktoren, denn statische Member sind unabhängig davon, ob die Klasse instanziierbar ist.

Das ist besonders praktisch für Registrierungsmuster: Eine abstrakte Basisklasse definiert eine abstrakte statische Methode wie fromJSON, jede Subklasse implementiert sie konkret, und eine generische Deserialisierungsfunktion kann diese Methode über den Konstruktortyp aufrufen, ohne die Basisklasse selbst zu instanziieren.

Der Zugriff auf statische Member über einen abstract new Typ ist ein Bereich, in dem sich die Typisierung deutlich von rein strukturellen Ansätzen mit Objektliteralen unterscheidet: Man bekommt echte Klassensemantik inklusive statischer Vererbung, ohne die Instanziierbarkeit vorauszusetzen.


abstract class Serializable {
  abstract toJSON(): unknown;
  static fromJSON(_data: unknown): Serializable {
    throw new Error("Muss in Subklasse implementiert werden");
  }
}

class User extends Serializable {
  constructor(public name: string) {
    super();
  }
  toJSON(): unknown {
    return { name: this.name };
  }
  static fromJSON(data: { name: string }): User {
    return new User(data.name);
  }
}

function deserialize<T extends Serializable>(
  ctor: abstract new (...args: any[]) => T,
  data: unknown,
): T {
  // Zugriff auf die statische Methode über den Konstruktortyp,
  // ohne "ctor" jemals selbst zu instanziieren.
  return (ctor as unknown as typeof Serializable).fromJSON(data) as T;
}

7. Typische Fallstricke

Der häufigste Fehler entsteht, wenn man versucht, direkt auf einer Variable vom Typ abstract new (...) => T mit new eine Instanz zu erzeugen. Der Compiler verbietet das korrekt und konsequent, denn zur Compilezeit ist nicht bekannt, ob der zugrunde liegende Wert konkret oder abstrakt ist. Der Fehler lautet sinngemäß, dass sich abstrakte Konstruktortypen nicht mit new aufrufen lassen. Ein Versuch, diese Regel per Type Assertion zu umgehen, hebelt zwar den Compiler aus, führt aber bei einer tatsächlich abstrakten Klasse zu einem echten Laufzeitfehler.

Ein zweiter Fallstrick ist die Vermischung von abstract new als Parametertyp mit der Erwartung, dass Aufrufer automatisch eine konkrete Subklasse mitliefern. TypeScript erzwingt das nicht von selbst: Wenn eine Funktion sowohl eine abstrakte Klasse als Vertrag als auch einen separaten konkreten Konstruktor benötigt, müssen beide explizit als getrennte Parameter typisiert werden, so wie im Factory Beispiel weiter oben gezeigt.

Ein dritter, subtilerer Fallstrick betrifft die Zuweisungsrichtung: Entwickler erwarten manchmal, dass sich ein abstract new Typ auch einem konkreten new Typ zuweisen lässt, weil intuitiv jede konkrete Klasse ja auch abstrakte Eigenschaften erfüllt. Das Typsystem erlaubt diese Zuweisung aber bewusst nicht in dieser Richtung, weil sonst die Garantie der Instanziierbarkeit verloren ginge.


abstract class Base {
  abstract doWork(): void;
}

function run(ctor: abstract new () => Base) {
  // Fehler: Ein abstrakter Konstruktortyp kann nicht mit "new" aufgerufen werden.
  const instance = new ctor();
  instance.doWork();
}

function runUnsafe(ctor: abstract new () => Base) {
  const AnyCtor = ctor as new () => Base;
  const instance = new AnyCtor(); // kompiliert, crasht aber zur Laufzeit,
  // sofern "ctor" tatsächlich eine abstrakte Klasse ist.
  instance.doWork();
}

8. Best Practices im Umgang mit abstract new

Nutzen Sie abstract new (...) => T immer dann, wenn eine Funktion eine Klasse rein als typsicheren Vertrag behandelt, etwa um deren statische Member zu lesen, sie als Constraint für generische Typen zu verwenden oder sie als Basis für eine extends Erweiterung heranzuziehen. Sobald die Funktion die Klasse tatsächlich instanziieren muss, gehört ein zusätzlicher, konkreter new (...) => T Parameter dazu, der explizit von der abstrakten Basisklasse ableitet.

Definieren Sie wiederverwendbare Type Aliase wie type Constructor<T> = new (...args: any[]) => T und type AbstractConstructor<T> = abstract new (...args: any[]) => T zentral in einem gemeinsamen Utility Modul. Das reduziert Wiederholung und macht die Absicht an jeder Verwendungsstelle sofort sichtbar, ohne dass jede Datei den vollen Signaturtyp erneut ausschreiben muss.

Vermeiden Sie Type Assertions, um die Trennung zwischen abstrakt und konkret künstlich aufzuheben. Wenn der Compiler an einer Stelle einen konkreten Konstruktor verlangt, ist das fast immer ein Signal, dass die Funktion tatsächlich einen zweiten, konkreten Parameter benötigt, statt den bestehenden Typ per Assertion zu erzwingen.


// utility-types.ts
export type Constructor<T = object> = new (...args: any[]) => T;
export type AbstractConstructor<T = object> =
  abstract new (...args: any[]) => T;

// container.ts
import type { AbstractConstructor, Constructor } from "./utility-types";

class Container {
  private bindings = new Map<AbstractConstructor, Constructor>();

  register<T extends object>(
    contract: AbstractConstructor<T>,
    implementation: Constructor<T>,
  ): void {
    this.bindings.set(contract, implementation);
  }

  resolve<T extends object>(contract: AbstractConstructor<T>): T {
    const implementation = this.bindings.get(contract) as Constructor<T>;
    return new implementation();
  }
}

9. Fazit: Wann abstract new (...) => T der richtige Typ ist

Die abstract construct signature schließt eine reale Lücke im Typsystem, die vor TypeScript 4.2 nur mit unsauberen Umwegen umgangen werden konnte. Überall dort, wo eine Funktion eine Klasse als reinen Bauplan oder Vertrag behandelt, ohne sie selbst zu instanziieren, ist abstract new (...) => T der genauere und ehrlichere Typ als das konkrete new (...) => T.

Sobald tatsächlich instanziiert werden muss, bleibt der konkrete Konstruktortyp weiterhin die richtige Wahl, häufig als zusätzlicher, separat typisierter Parameter neben dem abstrakten. Diese klare Trennung zwischen Vertrag und Instanziierbarkeit macht Factory Funktionen, Mixins und Dependency Injection Container nicht nur typsicherer, sondern auch selbstdokumentierend: Wer den Typ eines Parameters liest, weiß sofort, ob eine Instanziierung an dieser Stelle möglich ist oder nicht.

Merkmal new (...) => T abstract new (...) => T
Akzeptiert konkrete Klassen Ja Ja
Akzeptiert abstrakte Klassen Nein, Compilerfehler Ja
Direkter new-Aufruf erlaubt Ja Nein, Compilerfehler
Zuweisbar an new (...) => T Ja, zu sich selbst Nein, nur umgekehrt
Typischer Einsatzzweck Direkte Instanziierung Vertrag, Constraint, Mixin-Basis
Zugriff auf statische Member Ja Ja

Mironsoft

TypeScript-Migration, Typsicherheit und Team-Onboarding

JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?

Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.

Migrations-Fahrplan

Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.

Strict-Mode-Einführung

tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.

Team-Onboarding

Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.

10. Zusammenfassung

Abstract Construct Signatures

Seit Version

TypeScript 4.2

Kernidee

Konstruktortyp, der abstrakte und konkrete Klassen akzeptiert

Direkte Instanziierung

Nicht erlaubt, Compiler verbietet new auf abstract new

Haupteinsatz

Factory Funktionen, Mixins, DI Container

11. FAQ: Abstract Construct Signatures

1Was genau ist eine abstract construct signature in TypeScript?
Eine abstract construct signature ist ein Funktionstyp der Form abstract new (...args) => T. Sie beschreibt einen Konstruktor, der von einer abstrakten oder einer konkreten Klasse stammen darf. Im Gegensatz zum regulären Konstruktortyp new (...args) => T garantiert sie nicht, dass der beschriebene Wert direkt mit new instanziiert werden kann.
2Warum lehnt TypeScript new (...) => T bei abstrakten Klassen ab?
Weil new (...) => T eine Instanziierungsgarantie ausdrückt: Der Compiler stellt sicher, dass jeder Wert dieses Typs tatsächlich mit new aufgerufen werden kann. Eine abstrakte Klasse erfüllt diese Garantie nicht, da sie zur Laufzeit nicht direkt instanziierbar ist, deshalb verweigert der Compiler die Zuweisung schon zur Compilezeit.
3Kann ich auf einem abstract new Typ direkt new aufrufen?
Nein. Der Compiler verbietet den direkten new Aufruf auf einer Variable oder einem Parameter vom Typ abstract new (...) => T, weil zur Compilezeit nicht feststeht, ob der konkrete Wert dahinter instanziierbar ist. Man muss stattdessen sicherstellen, dass an der Stelle der tatsächlichen Instanziierung ein konkreter Konstruktor vorliegt.
4Wie kombiniere ich abstract new mit generischen Constraints?
Die übliche Formulierung lautet T extends abstract new (...args: any[]) => any. Dieser Constraint akzeptiert jeden Konstruktor, unabhängig davon, ob er abstrakt oder konkret ist, und unabhängig von der Anzahl der Argumente. Mit InstanceType lässt sich anschließend der zugehörige Instanztyp extrahieren.
5Worin unterscheidet sich abstract new von der Signatur klassischer Mixin-Funktionen?
Klassische Mixin Funktionen verwenden new (...args: any[]) => {}, weil sie die übergebene Klasse per extends erweitern und am Ende eine neue, konkrete Klasse erzeugen wollen. Moderne Mixin Bibliotheken nutzen teils abstract new (...args: any[]) => any als Constraint, um auch abstrakte Basisklassen als Ausgangspunkt zuzulassen, solange das Ergebnis der Erweiterung konkret bleibt.
6Kann ich statische Methoden über einen abstract new Typ aufrufen?
Ja. Statische Methoden und Properties eines Konstruktors sind unabhängig von der Instanziierbarkeit der Klasse und lassen sich über einen abstract new Typ genauso ansprechen wie über einen konkreten Konstruktortyp, solange sie im Typ deklariert sind.
7Wie baue ich eine Factory-Funktion, die eine abstrakte Basisklasse und eine konkrete Subklasse gleichzeitig erwartet?
Man definiert zwei getrennte Parameter: einen vom Typ abstract new (...) => T für die Basisklasse als Vertrag, und einen vom Typ new (...) => T für die tatsächlich zu instanziierende Subklasse. Innerhalb der Funktion wird ausschließlich der konkrete Parameter mit new aufgerufen.
8Ist es sicher, einen abstract new Typ per Type Assertion in new (...) => T umzuwandeln?
Nein, das ist ein bewusstes Umgehen der Typprüfung. Wenn der zugrunde liegende Wert tatsächlich eine abstrakte Klasse ist, führt der anschließende new Aufruf zu einem echten Laufzeitfehler. Type Assertions sollten hier nur verwendet werden, wenn durch die umgebende Logik zweifelsfrei sichergestellt ist, dass es sich um eine konkrete Klasse handelt.
9Funktioniert abstract new auch mit abstrakten statischen Membern?
Ja, seit TypeScript 4.2 können abstrakte Klassen abstrakte statische Member deklarieren. Ein Konstruktortyp vom Typ abstract new (...) => T kann diese statischen Member typsicher referenzieren, während konkrete Subklassen sie tatsächlich implementieren müssen.
10Wann sollte ich lieber new (...) => T statt abstract new (...) => T verwenden?
Immer dann, wenn eine Funktion die übergebene Klasse tatsächlich mit new instanziieren muss und keine abstrakten Basisklassen als gültige Eingabe akzeptieren soll. Ist die Klasse hingegen nur ein Vertrag, eine Constraint-Grundlage oder eine Mixin-Basis, ist abstract new (...) => T die präzisere und sicherere Wahl.