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.
Inhaltsverzeichnis
- 1. Das Problem: Eingabetyp bestimmt Rückgabetyp
- 2. Grundsyntax: Overload-Signaturen und Implementierungssignatur
- 3. Auflösungsreihenfolge: Die erste passende Signatur gewinnt
- 4. Overloads bei Klassenmethoden und Konstruktoren
- 5. Overloads versus Union-Types und Generics
- 6. Typische Fallstricke bei Overloads
- 7. Overloads in .d.ts-Deklarationsdateien
- 8. Best Practices für den Einsatz von Overloads
- 9. Vergleich der Ansätze und Fazit
- 10. Zusammenfassung
- 11. FAQ
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