Callable und Newable Interfaces gehören zu den eher unauffälligen Werkzeugen des TypeScript-Typsystems — viele Entwickler nutzen sie täglich, ohne ihren Namen zu kennen. Hinter den Kulissen vieler Library-Typen — von Date über jQuery bis hin zu Factory-Funktionen, die Klassen als Argument annehmen — stecken genau diese Konstrukte. Eine Call Signature beschreibt ein Interface, das wie eine Funktion aufgerufen werden kann; eine Construct Signature beschreibt eines, das mit dem new-Operator instanziiert wird. Beide nutzen, dass Funktionen in JavaScript Objekte sind und folglich gleichzeitig callable sein und Properties tragen können. Wer sie versteht, durchschaut auf einen Schlag die Typisierung von „Klasse als Parameter", erkennt das Muster hinter hybriden Konstrukten und kann Wrapper-Funktionen mit Methoden sauber modellieren. Dieser Artikel zeigt die Syntax, die typischen Patterns und die Grenzen — inklusive der Stelle, an der ein schlichtes typeof MyClass die elegantere Lösung ist.

Funktion als Interface — die Call Signature

Eine Call Signature beschreibt ein Interface, das wie eine Funktion aufgerufen werden kann. Syntaktisch sieht sie aus wie eine Methoden-Signatur ohne Namen — Parameter-Liste, Doppelpunkt, Rückgabe-Typ. Der entscheidende Unterschied zur klassischen Funktions-Typ-Notation (type X = (...) => ...): Die Call Signature steht in einem Objekt-Typ und kann mit Properties kombiniert werden.

ts
// Call Signature in einem Interface — Doppelpunkt vor dem Return-Typ.
interface Greeter {
  (name: string): string;
}

// Äquivalente Funktions-Typ-Notation — Pfeil vor dem Return-Typ.
type GreeterType = (name: string) => string;

// Beide sind zuweisbar — der Compiler sieht sie als strukturell gleich.
const greetA: Greeter     = (name) => `Hallo, ${name}!`;
const greetB: GreeterType = (name) => `Hallo, ${name}!`;

greetA("Welt");  // "Hallo, Welt!"
greetB("Welt");  // "Hallo, Welt!"

Funktional sind beide Schreibweisen identisch — die Wahl ist Stil-Frage. Die Interface-Variante eröffnet jedoch eine Möglichkeit, die der Funktions-Typ-Notation verschlossen bleibt: zusätzliche Properties auf dem callable Objekt. Genau dort wird die Call Signature interessant.

Call Signature mit Properties

JavaScript-Funktionen sind Objekte — sie können beliebige Properties tragen. Ein klassisches Muster ist die Funktion mit einem .cache- oder .version-Feld. TypeScript modelliert solche Konstrukte über Interfaces mit Call Signature plus Properties.

ts
// Interface: callable UND Property-Träger.
interface CachingFn {
  (key: string): string;          // Call Signature
  cache: Map<string, string>;     // Property auf der Funktion selbst
  hits: number;                   // Property, zur Statistik
}

// Konstruktion: zuerst die nackte Funktion, dann die Properties anhängen.
const lookup = ((key: string) => {
  const cached = lookup.cache.get(key);
  if (cached !== undefined) {
    lookup.hits++;                // Property-Zugriff auf die Funktion selbst.
    return cached;
  }
  const computed = key.toUpperCase();
  lookup.cache.set(key, computed);
  return computed;
}) as CachingFn;

lookup.cache = new Map();
lookup.hits  = 0;

lookup("hallo");   // "HALLO"
lookup("hallo");   // "HALLO" (aus dem Cache)
lookup.hits;       // 1

Die as CachingFn-Assertion ist hier notwendig, weil die nackte Pfeil-Funktion vor der Property-Zuweisung noch keine cache- und hits-Felder hat. Wer reine satisfies-Säule präferiert, schreibt das Pattern als gefüllten Object-Literal-Konstruktor — siehe Abschnitt zu Memoize. Wichtig ist die konzeptionelle Aussage: Ein Interface mit Call Signature beschreibt nicht „eine Funktion ODER ein Objekt", sondern „eine Funktion, die gleichzeitig Objekt ist".

Newable Interface — die Construct Signature

Eine Construct Signature beschreibt Konstrukte, die mit new aufgerufen werden — also Konstruktoren. Sie sieht aus wie eine Call Signature, beginnt aber mit dem Schlüsselwort new.

ts
// Die instanzierte Form — was nach `new Clock(...)` herauskommt.
interface Clock {
  hour: number;
  tick(): void;
}

// Die Konstruktor-Form — was VOR `new` steht.
interface ClockConstructor {
  new (h: number): Clock;         // `new` macht das zur Construct Signature.
}

// Eine konkrete Klasse, die der Konstruktor-Form genügt:
class DigitalClock implements Clock {
  hour: number;
  constructor(h: number) { this.hour = h; }
  tick() { this.hour = (this.hour + 1) % 24; }
}

// `DigitalClock` selbst (als Klassen-Referenz) ist der Konstruktor.
const Ctor: ClockConstructor = DigitalClock;
const c = new Ctor(10);           // c: Clock
c.tick();

Ohne das new-Keyword wäre (h: number): Clock eine gewöhnliche Call Signature — also eine Factory-Funktion, die ein Clock zurückgibt. Erst new macht aus der Signatur die Beschreibung eines Konstruktors. Beide Schreibweisen sind nicht zuweisbar zueinander: Eine Klasse ist kein Aufruf, und eine Factory ist keine Klasse.

Klassen-Constructors typisieren

Der wichtigste Anwendungsfall für Construct Signatures ist das Factory-Pattern: Eine Funktion bekommt eine Klasse als Argument und instanziiert sie. Ohne Construct Signature wäre der Typ der Klasse-als-Wert nicht sauber ausdrückbar.

ts
interface Animal {
  name: string;
  speak(): string;
}

// Construct Signature als Parameter-Typ — beschreibt die Klasse selbst.
type AnimalCtor = new (name: string) => Animal;

// Factory: bekommt die Klasse, gibt eine Instanz zurück.
function makeAnimal(Ctor: AnimalCtor, name: string): Animal {
  return new Ctor(name);
  //     ^^^ erlaubt nur, weil `Ctor` als `new (...) => Animal` getypt ist.
}

class Dog implements Animal {
  constructor(public name: string) {}
  speak() { return `${this.name}: Wuff!`; }
}

class Cat implements Animal {
  constructor(public name: string) {}
  speak() { return `${this.name}: Miau.`; }
}

makeAnimal(Dog, "Rex").speak();    // "Rex: Wuff!"
makeAnimal(Cat, "Mimi").speak();   // "Mimi: Miau."

Beachte: AnimalCtor ist als type-Alias geschrieben, weil eine einzelne Construct Signature kompakter als Pfeil-Notation lesbar bleibt. In Interface-Form wäre dasselbe interface AnimalCtor &#123; new (name: string): Animal &#125;. Beide sind äquivalent; type mit Pfeil ist hier idiomatisch.

Hybride Interfaces

Ein hybrides Interface kombiniert Call Signature, Construct Signature, Properties und Methoden in einem einzigen Typ. Das Lehrbuch-Beispiel ist jQuery: Das globale $ ist callable ($("div")), newable (new $.Deferred()), trägt Properties ($.fn, $.version) und hat Methoden ($.each(...)).

ts
interface JQueryElement { length: number; /* ... */ }
interface JQueryDeferred { resolve(): void; /* ... */ }

// Das hybride Interface — alles in einem Typ.
interface JQueryStatic {
  // 1) Call Signature — `$("div")` wählt Elemente aus.
  (selector: string): JQueryElement;

  // 2) Construct Signature — `new $.Deferred()` erzeugt ein Deferred.
  new (selector: string): JQueryElement;

  // 3) Property — `$.version` ist ein String.
  version: string;

  // 4) Methode — `$.each(arr, fn)` iteriert.
  each<T>(arr: T[], fn: (i: number, item: T) => void): void;

  // 5) Verschachteltes Property-Objekt — `$.fn.extend(...)`.
  fn: {
    extend(plugin: object): void;
  };
}

declare const $: JQueryStatic;

$("div").length;                // Call Signature
const el = new $("span");       // Construct Signature
$.version;                      // Property
$.each([1, 2, 3], (i, n) => {}); // Methode
$.fn.extend({ myPlugin() {} }); // verschachtelt

Direkt zu schreiben sind hybride Interfaces selten nötig — meist signalisieren sie, dass du gerade eine Library-API modellierst, die die Grenzen normaler Strukturen überschreitet. Im eigenen Code lohnt es sich, zuerst zu fragen, ob ein einfacheres Design (Namespace-Objekt, separate Klasse, plain Factory) den hybriden Typ überfluessig macht.

Overloads über mehrere Call Signatures

Ein Interface kann mehrere Call Signatures gleichzeitig deklarieren — das ist die Interface-Variante des Funktions-Overloadings. Der Compiler probiert die Signaturen von oben nach unten und nimmt die erste, die passt. Die Reihenfolge ist deshalb entscheidend: spezifisch vor allgemein.

ts
// Stringifier: nimmt entweder eine Zahl oder einen Boolean.
interface Stringifier {
  (n: number): string;            // Overload 1 — spezifisch für number
  (b: boolean): string;           // Overload 2 — spezifisch für boolean
}

const stringify: Stringifier = (value: number | boolean) =>
  typeof value === "number" ? value.toFixed(2) : value ? "ja" : "nein";

stringify(3.14);   // "3.14"   — Overload 1
stringify(true);   // "ja"     — Overload 2

// Reihenfolgen-Falle:
interface Bad {
  (x: unknown): string;           // matched ALLES — fataler erster Treffer
  (x: number): string;            // wird nie erreicht
}

Die Implementierung muss eine Vereinigung aller Overload-Signaturen abdecken — hier number | boolean. Der Compiler prüft beim Aufruf nur die deklarierten Overloads (Top-Down), nicht die Implementierungs-Signatur. Faustregel: Spezifischere Signaturen zuerst, breitere danach. Sonst „schluckt" die erste Signatur alle Aufrufe und die folgenden bleiben tot.

Praxis: Memoize-Wrapper

Das nützlichste Real-World-Pattern für hybride Interfaces ist der Memoize-Wrapper — eine Funktion, die das Ergebnis früherer Aufrufe zwischenspeichert und zusätzlich eine clear()-Methode zum Leeren des Caches anbietet.

ts
// Generisches hybrides Interface: callable + clear-Methode + Statistik.
interface Memoized<Args extends unknown[], R> {
  (...args: Args): R;             // Call Signature mit Spread für beliebige Argumente
  clear(): void;                  // leert den internen Cache
  readonly size: number;          // wie viele Einträge liegen aktuell im Cache
}

function memoize<Args extends unknown[], R>(
  fn: (...args: Args) => R
): Memoized<Args, R> {
  const cache = new Map<string, R>();

  // Innere Funktion deckt die Call Signature ab.
  const wrapped = ((...args: Args): R => {
    const key = JSON.stringify(args);   // simpler Schlüssel — reicht für Primitives
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  }) as Memoized<Args, R>;

  // Properties und Methoden auf das Funktions-Objekt heften.
  wrapped.clear = () => cache.clear();
  Object.defineProperty(wrapped, "size", {
    get: () => cache.size,        // dynamisch — kein Snapshot.
  });

  return wrapped;
}

const slowAdd = (a: number, b: number) => {
  // Simulation einer teuren Berechnung.
  return a + b;
};

const fastAdd = memoize(slowAdd);
fastAdd(1, 2);     // 3  (berechnet)
fastAdd(1, 2);     // 3  (aus dem Cache)
fastAdd.size;      // 1
fastAdd.clear();
fastAdd.size;      // 0

Der Trick: Memoized<Args, R> ist callable (Call Signature mit generischen Argumenten) und trägt eine clear-Methode plus eine size-Property. Die as-Assertion brücken den Moment, in dem das Funktions-Objekt bereits callable, aber noch nicht ausgestattet ist. Danach übernimmt der Typ und der Aufrufer sieht eine klar dokumentierte API.

Praxis: React-Komponente als Konstruktor-Pattern

Vor Hooks war eine React-Class-Component der Standard — und ihre Typisierung greift unter der Haube auf eine Construct Signature zurück. Ein Higher-Order-Component, das eine Class-Component als Argument bekommt, beschreibt diesen Parameter über new (...) =>.

ts
// Vereinfachtes React-Component-Modell.
interface ReactComponent<P> {
  props: P;
  render(): unknown;
}

// Construct Signature: nimmt Props im Konstruktor, erzeugt Component.
type ComponentClass<P> = new (props: P) => ReactComponent<P>;

// HOC: hebt Props an, gibt eine neue Component-Klasse zurück.
function withLogging<P>(WrappedClass: ComponentClass<P>): ComponentClass<P> {
  // Neue Klasse, die die übergebene Klasse umhüllt.
  return class extends (WrappedClass as any) {
    constructor(props: P) {
      super(props);
      console.log("Mounted:", WrappedClass.name);
    }
  } as ComponentClass<P>;
}

class Button implements ReactComponent<{ label: string }> {
  constructor(public props: { label: string }) {}
  render() { return `<button>${this.props.label}</button>`; }
}

const LoggedButton = withLogging(Button);
const instance = new LoggedButton({ label: "Klick" });
// Console: "Mounted: Button"
instance.render();

Die ComponentClass<P>-Definition entspricht in vereinfachter Form dem, was @types/react als ComponentClass<P> exportiert. Genau dieses Muster — Construct Signature mit Generic — taucht in jedem Framework auf, das Klassen als Plug-Ins akzeptiert: Angular-Module, NestJS-Provider, jede Form von Dependency Injection mit Klassen-Tokens.

Construct Signature vs. typeof Class

Wenn die Klasse, deren Konstruktor du typisieren willst, bereits existiert und im Scope erreichbar ist, ist typeof MyClass fast immer die einfachere Wahl. typeof auf einer Klasse liefert deren statische Seite — inklusive Konstruktor-Signatur und aller statischen Member.

ts
class Logger {
  static defaultLevel = "info";
  constructor(public name: string) {}
  log(msg: string) { console.log(`[${this.name}] ${msg}`); }
}

// Variante A — explizite Construct Signature: nur Konstruktor erfasst.
type LoggerCtorA = new (name: string) => Logger;

// Variante B — `typeof Logger`: Konstruktor PLUS statische Member.
type LoggerCtorB = typeof Logger;

function makeA(C: LoggerCtorA) {
  return new C("a");
  // C.defaultLevel  // Fehler — Construct Signature kennt keine Statics.
}

function makeB(C: LoggerCtorB) {
  const l = new C("b");
  console.log(C.defaultLevel);   // OK — `typeof` liefert auch Statics.
  return l;
}

Faustregel: Wenn die Klasse existiert, nimm typeof Class. Eine explizite Construct Signature lohnt sich nur, wenn die Klasse zur Compile-Zeit noch nicht bekannt ist — also bei generischen Factories, Plug-In-Systemen, oder wenn du bewusst nur den Konstruktor-Vertrag exponieren willst und Statics ausblenden möchtest.

Edge Cases und Stolperfallen

Drei Punkte, die in der Praxis regelmäßig aufschlagen:

ts
// 1) `abstract` Klassen passen NICHT auf eine normale Construct Signature.
abstract class Shape { abstract area(): number; }

type ShapeCtor = new () => Shape;
// const c: ShapeCtor = Shape;
//                      ^^^^^ Fehler: abstrakte Klasse ist nicht newable.

// Lösung: `abstract new (...)`-Signatur seit TypeScript 4.2:
type AbstractShapeCtor = abstract new () => Shape;
const ac: AbstractShapeCtor = Shape;        // OK

// 2) Call Signature OHNE `new` matched KEINE Klassen.
type Factory = (n: string) => Date;
class MyDate {}
// const f: Factory = MyDate;
//                    ^^^^^^ Fehler — Klasse ist nur `new`-fähig.

// 3) Reiner Funktions-Typ kann KEINE Properties tragen.
type Fn = (n: number) => number;
const fn: Fn = (n) => n * 2;
// fn.label = "x";   // Fehler — Property auf Fn nicht zulässig.
//                   // Lösung: Interface mit Call Signature + Property.

Die abstract new-Variante (TypeScript 4.2+) ist ein häufig übersehenes Detail: Sie erlaubt es, einer Factory abstrakte wie konkrete Klassen zu übergeben — typisch in Dependency-Injection-Systemen, in denen die Implementierung erst zur Laufzeit feststeht. Beim Punkt 3 wird der Kern der ganzen Sektion noch einmal klar: Call Signatures in Interfaces existieren genau deshalb, weil reine Funktions-Typ-Notationen keine Properties zulassen.

Besonderheiten

Call Signature im Interface vs. type X = (...) => ... — funktional gleich, Stil-Frage.

Solange keine zusätzlichen Properties am Funktions-Objekt hängen, sind interface Greeter { (n: string): string } und type Greeter = (n: string) => string strukturell identisch. Der Compiler unterscheidet sie nicht. Welche Variante du wählst, ist Code-Style: Die Pfeil-Notation ist kürzer, das Interface wächst besser mit, wenn später Properties hinzukommen.

Funktionen sind Objekte in JavaScript — deshalb können Interfaces sie modellieren.

Eine Funktion in JS ist ein vollwertiges Objekt mit Prototyp, Properties und allem, was dazugehört. function f() {}; f.x = 1; ist legitimer Code. TypeScript bildet diese Realität ab, indem Interfaces gleichzeitig callable sein und Properties tragen können. Genau das ist der konzeptionelle Boden, auf dem Call Signatures stehen.

Das new-Keyword in der Construct Signature ist Pflicht — sonst Call Signature.

Ohne new beschreibt (s: string): T eine Factory-Funktion. Mit new wird daraus new (s: string): T — die Beschreibung eines Konstruktors. Beide sind nicht zuweisbar zueinander: Eine Klasse ist keine Factory, und eine Factory ist keine Klasse. Wer einer Funktion eine Klasse zuweist (oder umgekehrt), bekommt prompt einen Compiler-Fehler.

Hybride Interfaces sind selten direkt nötig — meist signalisieren sie Library-Magie.

Wer in eigenem Code ein Interface schreibt, das callable, newable, Property-Träger und Methoden-Container in einem ist, sollte zweimal nachdenken. Solche Konstrukte sind nützlich für die Modellierung externer Libraries (jQuery, Express, Lodash), bevorzugen im eigenen Code aber meist eine sauberere Trennung in Namespace-Objekt und Klasse.

typeof MyClass als Alternative zur Construct Signature, wenn die Klasse existiert.

typeof Logger liefert die statische Seite der Klasse — Konstruktor-Signatur und alle statischen Member. Eine explizite new (...) => Logger-Signatur kennt nur den Konstruktor, blendet statische Member aus. Faustregel: existiert die Klasse zur Compile-Zeit, ist typeof die einfachere und vollständigere Wahl.

jQuery-$ ist das Lehrbuch-Beispiel für hybride Interfaces.

Das globale $ ist callable ($("div")), newable (new $.Deferred()), trägt Properties ($.fn, $.version) und hat Methoden ($.each, $.ajax). Genau diese Vielfalt zwang die TypeScript-Community 2012 dazu, das Typsystem um hybride Interfaces zu erweitern. Die JQueryStatic-Definition in @types/jquery ist bis heute eine der lehrreichsten Studien.

Overloads über Call Signatures: Compiler matched Top-Down — Reihenfolge zählt.

Mehrere Call Signatures hintereinander funktionieren wie Funktions-Overloads. Der Compiler probiert sie von oben nach unten und nimmt die erste passende. Spezifischere Signaturen müssen daher VOR allgemeinen stehen. Eine Signatur mit unknown oder any als erstes Element macht alle folgenden Signaturen unerreichbar — ein häufiger, schwer zu entdeckender Bug.

Memoize-Wrapper ist das nützlichste Real-World-Pattern.

Funktionen, die zusätzlich eine clear()-Methode, eine size-Property oder Statistik-Felder anbieten, sind ein wiederkehrendes Pattern in Performance-Code. Das hybride Interface aus Call Signature und Methoden modelliert das präzise und gibt dem Aufrufer eine klare API — ohne dass die innere Implementierung als Klasse organisiert sein muss.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Interfaces

Zur Übersicht