Funktionsüberladung in TypeScript bedeutet, für eine Funktion mehrere Aufrufsignaturen zu schreiben, gefolgt von einer einzigen Implementierung. Jede Signatur kann andere Parametertypen mit einem anderen Rückgabetyp verbinden, und der Aufrufer bekommt den genauen.
Ohne die Überladungen würde parse für jeden Aufruf number | number[] zurückgeben, und one + 1 wäre ein Fehler, bis du das Ergebnis selbst eingeengt hättest.
Overload Signatures und die Implementierung
Eine überladene Funktion besteht aus zwei Teilen:
- Overload Signatures: Deklarationen ohne Rumpf, eine pro unterstützter Aufrufform. Nur diese Signaturen können Aufrufer verwenden.
- Die Implementierungssignatur: die letzte Deklaration, mit Rumpf. Ihre Parameter müssen alles akzeptieren, was die Überladungen akzeptieren, und ihr Rückgabetyp muss den Rückgabetyp jeder Überladung abdecken. Von außen ist sie unsichtbar.
Typen gibt es nur beim Kompilieren, zur Laufzeit existiert also eine JavaScript-Funktion. Die Implementierung muss ihre Argumente untersuchen (typeof, Array.isArray, arguments.length...), um zu entscheiden, was zu tun ist. Der Compiler prüft, dass Überladungen und Implementierung zusammenpassen:
function format(value: string): string;
function format(value: number): number {
return value;
}
// error TS2394: This overload signature is not compatible with its implementation signature.
Die Lösung ist, die Implementierung zu erweitern: function format(value: string | number): string | number.
Die Implementierungssignatur ist nicht aufrufbar
Diese Regel überrascht am meisten. Ein Aufruf muss für sich allein zu einer der Overload Signatures passen; TypeScript kombiniert sie nicht.
Der Compiler gibt aus:
index.ts(12,19): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
Type 'string' is not assignable to type 'string[]'.
Die Implementierung akzeptiert string | string[], aber Aufrufer sehen sie nicht. Füge eine dritte Überladung hinzu, die die Union nimmt und die Union zurückgibt, dann kompiliert der Aufruf und gibt [ 1, 2 ] aus:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
Unterschiedliche Anzahl von Parametern
Überladungen beschreiben auch Aufrufe mit unterschiedlicher Stelligkeit. Hier lässt sich ein Datum aus einem Zeitstempel oder aus Jahr, Monat und Tag bauen, aber nicht aus zwei Zahlen:
Eine einzige Signatur mit zwei optionalen Parametern würde makeDate(2024, 3) akzeptieren und stillschweigend das falsche Datum bauen. Die Überladungen machen daraus einen Compilerfehler (TS2575).
Die Reihenfolge zählt
TypeScript probiert die Überladungen von oben nach unten und nimmt die erste, die passt. Setze die spezifischsten Signaturen an den Anfang. Eine breite Überladung weit oben in der Liste schluckt die Aufrufe, die für die folgenden gedacht sind:
function describe(value: unknown): string; // matches everything
function describe(value: string): "text"; // never chosen
function describe(value: unknown): string {
return typeof value === "string" ? "text" : "other";
}
const d = describe("hi"); // d: string, not "text"
Vertausche die ersten beiden Signaturen, und describe("hi") ist als "text" typisiert.
Überladungen oder ein Union-Parameter?
Überladungen sind ihre zusätzlichen Zeilen wert, wenn der Rückgabetyp von den Argumenttypen abhängt. Ist das nicht so, ist eine Signatur mit Union-Parameter kürzer, leichter zu lesen und akzeptiert Union-Argumente, die Überladungen ablehnen würden.
| Verwenden | Wann |
|---|---|
| Einen Union-Parameter | Gleicher Rückgabetyp für jede Eingabe |
| Optionale Parameter | Die Aufrufformen unterscheiden sich nur durch nachfolgende Argumente, die frei weggelassen werden können |
| Überladungen | Der Rückgabetyp ändert sich mit den Argumenten, oder manche Kombinationen von Argumenten müssen abgelehnt werden |
| Ein Generic | Der Rückgabetyp wird aus dem Argumenttyp gebildet, etwa identity<T>(x: T): T |
Ein Generic mit einem Conditional Type kann manche Sätze von Überladungen als eine Signatur ausdrücken, aber bei zwei oder drei Fällen sind Überladungen meist leichter zu lesen.
Überladene Methoden und Konstruktoren
Methoden verwenden in einer Klasse dasselbe Muster: Overload Signatures, dann die Methode mit Rumpf. Konstruktoren lassen sich genauso überladen.
Auch Interfaces und Objekttypen können Überladungen deklarieren, als mehrere Call Signatures oder mehrere gleichnamige Methodensignaturen. Viele eingebaute Funktionen sind so deklariert: Fahre im Editor mit der Maus über reduce eines Arrays, und es zeigt „+2 overloads“.
Häufig gestellte Fragen
Unterstützt TypeScript Funktionsüberladung?
Ja, auf Typebene. Du schreibst mehrere Overload Signatures (Deklarationen ohne Rumpf), gefolgt von einer Implementierung. Aufrufer sehen nur die Overload Signatures. Zur Laufzeit gibt es weiterhin nur eine JavaScript-Funktion, die Implementierung prüft die Argumente also selbst und behandelt jeden Fall.
Was bedeutet "No overload matches this call"?
Fehler TS2769: Die Argumente passen zu keiner der Overload Signatures. Die Implementierungssignatur zählt nicht mit, also scheitert ein Aufruf mit einem Union-Argument wie string | string[], selbst wenn die Implementierung es akzeptiert. Füge eine Überladung hinzu, die die Union nimmt, oder ersetze die Überladungen durch eine Signatur.
Wann sollte ich Überladungen statt eines Union-Typs verwenden?
Nimm Überladungen, wenn der Rückgabetyp davon abhängt, welche Argumenttypen übergeben werden, zum Beispiel string rein ergibt number raus, aber string[] rein ergibt number[] raus. Ist der Rückgabetyp für jede Eingabe gleich, ist eine einzige Signatur mit Union-Parameter einfacher und akzeptiert auch Union-Argumente.
Kann man Arrow Functions in TypeScript überladen?
Nicht mit der Syntax für Overload-Deklarationen, die nur für function-Deklarationen und Methoden funktioniert. Du kannst einer Variablen einen überladenen Typ mit mehreren Call Signatures geben, type Parse = { (s: string): number; (s: string[]): number[] }, aber eine Arrow Function daran zuzuweisen braucht meist eine Typ-Assertion, eine function-Deklaration ist also die sauberere Wahl.
Warum ist meine Overload Signature nicht mit ihrer Implementierungssignatur kompatibel?
Der Fehler TS2394 bedeutet, dass eine Überladung etwas akzeptiert oder zurückgibt, was die Implementierung nicht tut. Die Parameter der Implementierung müssen die Parameter jeder Überladung akzeptieren, und ihr Rückgabetyp muss mit dem jeder Überladung kompatibel sein. Die Implementierung zu erweitern (oft auf eine Union) behebt das.