Funktionsüberladung in TypeScript: Overloads richtig nutzen
AI generated
type
TypeScript · Funktionen
Funktionsüberladung (Overloads) in TypeScript richtig einsetzen
Mehrere Signaturen, eine Implementierung, präzise Typen

Overloads erlauben es, für eine einzelne Funktion mehrere aufrufbare Signaturen zu definieren, während im Hintergrund nur eine einzige Implementierung existiert. Richtig eingesetzt liefern sie deutlich präzisere Rückgabetypen als eine einzelne Signatur mit Union-Types, bringen aber auch eigene Fallstricke mit, etwa bei der Auflösungsreihenfolge oder bei unerreichbaren Signaturen.

10 Min. Lesezeit TypeScript Typsystem API-Design

1. Das Problem: Eingabetyp bestimmt Rückgabetyp

In vielen realen Funktionen hängt der Rückgabetyp direkt vom Typ eines Parameters ab. Ein parse, das bei einem String ein Array zurückgibt und bei einer Zahl eine einzelne Zahl, lässt sich mit einer einzigen Signatur nur unpräzise beschreiben. Nutzt man dafür einfach Union-Types für Parameter und Rückgabewert, verliert der Aufrufer die Information, welcher Eingabetyp zu welchem Ausgabetyp gehört, und muss selbst wieder Typprüfungen oder Type-Assertions einbauen.

Genau für diesen Fall gibt es Funktionsüberladung. Eine überladene Funktion besteht aus mehreren Overload-Signaturen, die jeweils eine konkrete Kombination aus Parametertypen und Rückgabetyp beschreiben, sowie einer einzigen Implementierungssignatur mit dem tatsächlichen Funktionskörper. TypeScript prüft bei jedem Aufruf, welche Overload-Signatur passt, und wählt darauf basierend den Rückgabetyp aus, ganz ohne Laufzeitkosten, denn zur Laufzeit existiert nur eine einzige JavaScript-Funktion.

2. Grundsyntax: Overload-Signaturen und Implementierungssignatur

Eine überladene Funktion besteht aus mindestens zwei Overload-Signaturen (Deklarationen ohne Funktionskörper) und genau einer Implementierungssignatur direkt darunter, die den Funktionskörper enthält. Die Implementierungssignatur muss so allgemein formuliert sein, dass sie alle Parameter- und Rückgabetypen der darüberliegenden Overload-Signaturen abdeckt, üblicherweise über Union-Types.

Wichtig ist: Die Implementierungssignatur selbst ist von außen nicht sichtbar und kann nicht direkt aufgerufen werden. Aufrufer sehen ausschließlich die Overload-Signaturen. Das folgende Beispiel zeigt eine Funktion, die bei einem String ein Array von Teilstrings liefert und bei einer Zahl das Doppelte der Zahl zurückgibt.


// Overload-Signaturen (keine Funktionskörper)
function parseInput(value: string): string[];
function parseInput(value: number): number;

// Implementierungssignatur (nicht direkt aufrufbar, deckt beide Fälle ab)
function parseInput(value: string | number): string[] | number {
  if (typeof value === "string") {
    return value.split(",").map((part) => part.trim());
  }
  return value * 2;
}

const a = parseInput("1, 2, 3"); // Typ: string[]
const b = parseInput(42);        // Typ: number

3. Auflösungsreihenfolge: Die erste passende Signatur gewinnt

TypeScript prüft die Overload-Signaturen in der Reihenfolge, in der sie im Code stehen, von oben nach unten. Sobald eine Signatur zur Anzahl und zu den Typen der übergebenen Argumente passt, wird diese Signatur für die Typprüfung des Aufrufs verwendet, auch wenn eine spätere Signatur ebenfalls passen würde. Diese Reihenfolge ist also kein Implementierungsdetail, sondern Teil des sichtbaren Vertrags der Funktion.

Das folgende Beispiel zeigt eine Funktion, die entweder einen Zeitstempel oder Jahr, Monat und Tag als Argumente akzeptiert. Da beide Signaturen mit numerischen Argumenten arbeiten, entscheidet allein die Anzahl der übergebenen Argumente, welche Signatur greift, und die Reihenfolge im Code sorgt dafür, dass der einzelne Zeitstempel zuerst geprüft wird.


function createDate(timestamp: number): Date;
function createDate(year: number, month: number, day: number): Date;
function createDate(a: number, b?: number, c?: number): Date {
  if (b === undefined) {
    return new Date(a);
  }
  return new Date(a, b - 1, c ?? 1);
}

createDate(1723027200000); // passt auf die erste Signatur
createDate(2026, 8, 7);    // passt auf die zweite Signatur

4. Overloads bei Klassenmethoden und Konstruktoren

Funktionsüberladung funktioniert nicht nur bei freistehenden Funktionen, sondern genauso bei Methoden und Konstruktoren von Klassen. Auch hier gilt: mehrere Overload-Signaturen direkt untereinander, gefolgt von genau einer Implementierung, die alle Fälle abdeckt. Bei Konstruktoren ist zu beachten, dass Constructor Property Promotion nur in der Implementierungssignatur genutzt werden kann, da nur diese einen Funktionskörper besitzt.

In der Praxis eignet sich dieses Muster gut für Klassen mit mehreren sinnvollen Initialisierungswegen, etwa einen einfachen Konstruktor mit nur der Basis-URL und einen erweiterten Konstruktor mit zusätzlichen Optionen.


class ApiClient {
  constructor(baseUrl: string);
  constructor(baseUrl: string, options: { timeoutMs: number });
  constructor(
    private baseUrl: string,
    private options: { timeoutMs: number } = { timeoutMs: 5000 }
  ) {}

  request(path: string): Promise<unknown>;
  request(path: string, method: "GET" | "POST"): Promise<unknown>;
  request(path: string, method: "GET" | "POST" = "GET"): Promise<unknown> {
    return fetch(`${this.baseUrl}${path}`, { method }).then((res) => res.json());
  }
}

const client = new ApiClient("https://api.example.com");
const timed = new ApiClient("https://api.example.com", { timeoutMs: 2000 });

5. Overloads versus Union-Types und Generics

Overloads sind nicht die einzige Möglichkeit, um typabhängiges Verhalten abzubilden. Bei einer einfachen Union-Type-Signatur bleibt der Rückgabetyp für den Aufrufer immer die volle Union, unabhängig davon, welcher konkrete Eingabetyp übergeben wurde. Generics mit Conditional Types können diese Präzision zwar ebenfalls herstellen, sind dafür aber deutlich abstrakter zu lesen und zu warten.

Als Faustregel gilt: Overloads eignen sich gut, wenn es eine kleine, feste Anzahl klar unterscheidbarer Eingabe-Ausgabe-Kombinationen gibt, die sich nicht sauber als generische Typbeziehung ausdrücken lassen. Generics und Conditional Types lohnen sich eher, wenn die Beziehung zwischen Eingabe- und Rückgabetyp tatsächlich für beliebig viele Typen gilt und sich als Formel schreiben lässt.


// Variante 1: Union-Type, Präzision geht verloren
function toArrayUnion(value: string | number): string[] | number[] {
  return typeof value === "string" ? value.split("") : [value];
}
const lostPrecision = toArrayUnion("abc"); // Typ: string[] | number[]

// Variante 2: Overloads, Präzision bleibt erhalten
function toArray(value: string): string[];
function toArray(value: number): number[];
function toArray(value: string | number): string[] | number[] {
  return typeof value === "string" ? value.split("") : [value];
}
const precise = toArray("abc"); // Typ: string[]

// Variante 3: Conditional Type, generisch für beliebige Fälle
type ToArrayResult<T> = T extends string ? string[] : number[];
function toArrayGeneric<T extends string | number>(value: T): ToArrayResult<T> {
  return (typeof value === "string" ? value.split("") : [value]) as ToArrayResult<T>;
}

6. Typische Fallstricke bei Overloads

Der häufigste Fehler ist der Versuch, die Implementierungssignatur direkt von außen aufzurufen. Das funktioniert nicht, denn nur die darüberliegenden Overload-Signaturen sind Teil der öffentlichen Schnittstelle. Ein Aufruf mit einer Argumentkombination, die zwar zur Implementierung passt, aber keiner Overload-Signatur entspricht, wird von TypeScript abgelehnt.

Ein zweiter klassischer Fehler ist eine falsche Reihenfolge, bei der eine sehr allgemeine Signatur vor einer spezifischeren steht. Da die erste passende Signatur gewinnt, wird die speziellere Signatur dann nie erreicht, und TypeScript meldet einen Fehler wegen einer nicht erreichbaren Overload-Signatur. Zusätzlich können Overloads einen expliziten this-Parameter als erstes Pseudo-Argument tragen, um den erwarteten Aufrufkontext zu typisieren, etwa bei Callback-basierten Browser- oder DOM-APIs. Dieser this-Parameter zählt nicht zur eigentlichen Argumentanzahl, muss aber in jeder Overload-Signatur konsistent gesetzt werden.


// Falsche Reihenfolge: "any" fängt alles ab, die zweite Signatur ist unerreichbar
function format(value: any): string;
function format(value: number): string; // Fehler: nicht erreichbar
function format(value: any): string {
  return String(value);
}

// this-Parameter in einer Overload-Signatur
interface ButtonHandlers {
  onClick(this: HTMLButtonElement, handler: () => void): void;
  onClick(this: HTMLButtonElement, event: "click", handler: () => void): void;
}

// Aufruf mit einer Kombination, die keine Overload-Signatur abdeckt
function log(message: string): void;
function log(code: number, message: string): void;
function log(a: string | number, b?: string): void {
  console.log(a, b);
}
log("Fehler", "Zusatz"); // Fehler: keine passende Overload-Signatur vorhanden

7. Overloads in .d.ts-Deklarationsdateien

In reinen Deklarationsdateien mit der Endung .d.ts existiert grundsätzlich kein Funktionskörper, weder für einzelne Funktionen noch für Overloads. Dort stehen ausschließlich die Overload-Signaturen selbst, ohne begleitende Implementierungssignatur, denn die eigentliche Implementierung liegt bereits als kompiliertes JavaScript vor oder wird von einer externen Bibliothek bereitgestellt.

Dieses Muster begegnet einem häufig bei Typdefinitionen für JavaScript-Bibliotheken, etwa wenn eine Funktion je nach Anzahl oder Typ der Argumente unterschiedliche Rückgabewerte liefert. Für Autoren eigener Bibliotheken ist wichtig, dass die Overload-Signaturen in der Deklarationsdatei exakt der Reihenfolge und den Typen der Overload-Signaturen in der zugehörigen Implementierungsdatei entsprechen müssen, sonst driften öffentliche Typen und tatsächliches Verhalten auseinander.


// math-utils.d.ts
export declare function clamp(value: number, min: number, max: number): number;
export declare function clamp(value: number, range: [number, number]): number;

// Verwendung in einer .ts-Datei, die die Deklaration importiert
import { clamp } from "./math-utils";

clamp(12, 0, 10);       // erste Signatur
clamp(12, [0, 10]);     // zweite Signatur

8. Best Practices für den Einsatz von Overloads

Overload-Listen sollten so kurz und so spezifisch wie möglich bleiben, im Idealfall zwei bis vier Signaturen. Werden es deutlich mehr, ist das oft ein Signal, dass ein Generic oder ein Conditional Type die Beziehung sauberer und wartbarer ausdrücken kann. Jede Overload-Signatur sollte außerdem eine klar unterscheidbare Argumentkombination beschreiben, damit die Auflösungsreihenfolge nicht zur Fehlerquelle wird.

Sinnvoll ist zudem, spezifischere Signaturen konsequent vor allgemeineren zu platzieren, die Implementierungssignatur so eng wie möglich, aber trotzdem alle Fälle abdeckend zu formulieren, sowie jede Overload-Signatur mit einem eigenen JSDoc-Kommentar zu versehen. So sieht jeder Aufrufer in der IDE sofort, welche Argumentkombinationen erlaubt sind und was sie jeweils zurückliefern, ohne die Implementierung lesen zu müssen.


/** Sucht ein einzelnes Element im DOM. */
function query(selector: string): Element | null;
/** Sucht alle passenden Elemente im DOM. */
function query(selector: string, all: true): NodeListOf<Element>;
function query(
  selector: string,
  all?: boolean
): Element | NodeListOf<Element> | null {
  return all
    ? document.querySelectorAll(selector)
    : document.querySelector(selector);
}

9. Vergleich der Ansätze und Fazit

Overloads, Union-Types, Generics und Conditional Types lösen alle das gleiche Grundproblem auf unterschiedliche Weise: eine präzise Typbeziehung zwischen Eingabe und Rückgabewert abzubilden. Welcher Ansatz passt, hängt vor allem davon ab, wie viele feste Fälle es gibt und ob sich die Beziehung als generische Formel formulieren lässt.

Für eine kleine, feste Anzahl klar getrennter Fälle sind Overloads oft die lesbarste Lösung, weil sie in der IDE als saubere, einzeln dokumentierte Signaturen erscheinen. Sobald die Anzahl der Fälle wächst oder die Beziehung zwischen Eingabe- und Rückgabetyp für beliebig viele Typen gelten soll, lohnt sich der Umstieg auf Generics oder Conditional Types, auch wenn diese für Einsteiger zunächst abstrakter wirken.

Ansatz Wann sinnvoll Typpräzision Wartungsaufwand
Overloads Wenige, klar getrennte Eingabe-Ausgabe-Fälle Sehr hoch pro Fall Steigt schnell mit der Anzahl der Signaturen
Union-Types Einfache Fälle ohne feste Typ-Beziehung Niedrig, Aufrufer muss selbst prüfen Gering, aber Typinformation geht verloren
Generics Verhalten ist für beliebige Typen strukturell gleich Hoch, folgt dem konkreten Typparameter Moderat, gut wartbar bei klarer Struktur
Conditional Types Rückgabetyp hängt formelhaft vom Eingabetyp ab Sehr hoch, auch für offene Typmengen Höher, erfordert mehr Typsystem-Erfahrung
.d.ts-Overloads Typisierung bestehender JS-Bibliotheken Hoch, ohne eigene Implementierung Muss synchron zur echten Implementierung bleiben

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

Funktionsüberladung

Kernidee

Mehrere Overload-Signaturen, eine Implementierungssignatur, ein Rückgabetyp pro Eingabetyp

Auflösung

Erste passende Overload-Signatur von oben nach unten gewinnt

Größter Fallstrick

Falsche Reihenfolge macht spezifische Signaturen unerreichbar

Alternative

Bei vielen oder offenen Fällen lieber Generics oder Conditional Types

11. FAQ: Funktionsüberladung

1Was ist der Unterschied zwischen Overload-Signatur und Implementierungssignatur?
Die Overload-Signaturen sind die öffentlich sichtbaren, aufrufbaren Deklarationen ohne Funktionskörper. Die Implementierungssignatur steht direkt darunter, enthält den eigentlichen Code und muss alle Parameter- und Rückgabetypen der Overload-Signaturen abdecken. Von außen ist ausschließlich über die Overload-Signaturen aufrufbar, die Implementierungssignatur selbst ist nicht direkt nutzbar.
2Warum kann ich die Implementierungssignatur nicht direkt aufrufen?
TypeScript blendet die Implementierungssignatur bewusst aus der öffentlichen Schnittstelle aus. Nur Aufrufe, die zu mindestens einer der darüberliegenden Overload-Signaturen passen, werden akzeptiert. Das verhindert, dass Aufrufer Argumentkombinationen nutzen, die zwar technisch im Funktionskörper funktionieren würden, aber nicht als offizieller Teil der API gedacht sind.
3Wie entscheidet TypeScript, welche Overload-Signatur bei einem Aufruf verwendet wird?
TypeScript prüft die Overload-Signaturen strikt in der Reihenfolge, in der sie im Quellcode stehen. Die erste Signatur, deren Parameteranzahl und Parametertypen zu den übergebenen Argumenten passen, wird verwendet, auch wenn eine spätere Signatur ebenfalls zutreffen würde. Die Reihenfolge im Code ist also entscheidend für das beobachtbare Verhalten.
4Was bedeutet eine unerreichbare Overload-Signatur?
Steht eine allgemeine Signatur, etwa mit dem Typ any oder einem breiten Union-Type, vor einer spezifischeren Signatur, wird die speziellere niemals ausgewählt, weil die allgemeinere bereits jeden passenden Aufruf abfängt. TypeScript meldet das als Fehler, weil die spätere Signatur faktisch toter Code ist.
5Können Konstruktoren einer Klasse ebenfalls überladen werden?
Ja, Konstruktoren funktionieren nach demselben Muster wie Methoden. Mehrere Overload-Signaturen für den Konstruktor stehen über der eigentlichen Implementierung, die alle Fälle abdeckt. Constructor Property Promotion kann dabei nur in der Implementierungssignatur genutzt werden, da nur diese einen Funktionskörper besitzt.
6Wann sollte ich Overloads statt Union-Types verwenden?
Immer dann, wenn der konkrete Eingabetyp den konkreten Rückgabetyp eindeutig bestimmt und diese Information für Aufrufer wichtig ist. Eine einzelne Signatur mit Union-Types liefert immer die volle Union als Rückgabetyp, unabhängig vom tatsächlich übergebenen Wert, während Overloads für jede Eingabe den exakt passenden Rückgabetyp liefern.
7Wann sind Generics oder Conditional Types die bessere Wahl gegenüber Overloads?
Sobald die Beziehung zwischen Eingabe- und Rückgabetyp für beliebig viele Typen nach demselben Muster gilt, lässt sie sich meist als generische Formel mit einem Conditional Type ausdrücken. Das spart eine wachsende Liste einzelner Overload-Signaturen und bleibt auch bei neuen Typen ohne Codeänderung korrekt.
8Was macht der this-Parameter in einer Overload-Signatur?
Ein als erstes Pseudo-Argument deklarierter this-Parameter typisiert den erwarteten Aufrufkontext einer Funktion, etwa das konkrete DOM-Element, auf dem ein Callback später ausgeführt wird. Er zählt nicht zur eigentlichen Parameteranzahl beim Aufruf, muss aber pro Overload-Signatur konsistent angegeben werden.
9Wie sehen Overloads in .d.ts-Deklarationsdateien aus?
In Deklarationsdateien stehen ausschließlich die Overload-Signaturen ohne begleitende Implementierungssignatur, da Deklarationsdateien grundsätzlich keinen Funktionskörper enthalten dürfen. Die tatsächliche Implementierung liegt entweder als kompiliertes JavaScript vor oder wird von der beschriebenen Bibliothek bereitgestellt.
10Wie viele Overload-Signaturen sind für eine Funktion sinnvoll?
Als Richtwert gelten zwei bis vier klar unterscheidbare Signaturen. Wächst die Liste deutlich darüber hinaus, wird die Funktion schwerer lesbar und wartbar, und es lohnt sich zu prüfen, ob ein Generic oder ein Conditional Type dieselbe Typpräzision mit weniger Code erreichen kann.