Warum geht der übergebene Typ bei any verloren?
Wenn Sie einen Parameter als any typisieren, prüft TypeScript nichts mehr von dem, was aus der Funktion kommt:
| |
Sie wissen, dass Sie ein Array von Zahlen übergeben haben. Der Compiler weiß es nicht, weil any ihm sagt, nicht mehr hinzusehen. n kommt als any zurück, also compiliert der Aufruf einer String-Methode auf einer Zahl klaglos und wirft beim Ausführen TypeError: n.toUpperCase is not a function.
Ein Generic benennt den Typ, statt ihn zu verwerfen:
| |
T ist ein Typparameter, ein Platzhalter, der bei jedem Aufruf gefüllt wird. firstElement([1, 2, 3]) füllt T mit number, der Rückgabetyp wird number, und der falsche Aufruf von .toUpperCase() schlägt in Ihrem Editor fehl statt in der Produktion. Sie schreiben die Funktion einmal, und jeder Elementtyp behält seine Prüfung.
Der Rest dieser Anleitung zeigt, wie Sie Generics nutzen, ohne die Signatur schwerer lesbar zu machen als den Bug, den sie verhindern.
Wie leitet TypeScript den Typparameter her?
Sie haben oben nicht firstElement<number>([1, 2, 3]) geschrieben, und das brauchen Sie selten. TypeScript vergleicht das Argument mit der deklarierten Form des Parameters (T[]) und löst T auf. Es ist dieselbe Inferenz, die const x = 5 als number typisiert, nur angewendet auf Typparameter.
Gibt es nichts, woraus sich etwas herleiten lässt, fällt T auf unknown zurück:
| |
Hat der Aufruf keinen Wert, aus dem sich etwas herleiten lässt, übergeben Sie das Typargument selbst in spitzen Klammern.
Die Inferenz kann auch schiefgehen, wenn zwei Argumente dasselbe T füllen und sich widersprechen:
| |
TypeScript wählt einen Kandidaten und meldet das andere Argument als Problem. Das kann zu einer Fehlermeldung führen, die einen Typ nennt, den Sie nie geschrieben haben. Liefert ein generischer Aufruf einen verwirrenden Fehler oder einen unerwarteten Typ, fahren Sie in Ihrem Editor über den Aufruf und lesen Sie, was für T hergeleitet wurde. Das erklärt das Problem meist schneller als das Lesen der Fehlermeldung.
Wie schränken Sie ein Generic mit extends und keyof ein?
Ein uneingeschränktes T kann alles sein. Innerhalb der Funktion können Sie dann nur das tun, was jeder Typ unterstützt – und das ist, abgesehen vom Weiterreichen des Werts, fast nichts. Die meisten echten generischen Funktionen müssen etwas über T voraussetzen, und mit extends drücken Sie das aus:
| |
K extends keyof T beschränkt K auf die Property-Namen des übergebenen Objekts, und T[K] liest den Typ dieser Property aus. Die Liste der gültigen Schlüssel kommt vom Objekt an der Aufrufstelle, nicht aus einer festverdrahteten Union. Ein Tippfehler wie "pay" wird so zu einem Compilerfehler statt zu einem stillen undefined.
Die Einschränkung kann auch eine einfache Form sein:
| |
Strings und Arrays haben beide eine length, also bestehen beide. Eine Zahl nicht, und der Aufruf wird abgelehnt, bevor der Funktionskörper überhaupt läuft. Da der Rückgabetyp T ist und nicht { length: number }, gibt Ihnen logLength("hello") trotzdem einen string zurück.
Wie funktionieren generische Interfaces und Klassen?
Ein generisches Interface ist eine Form mit einem Typparameter:
| |
Eine generische Klasse teilt ihren Typparameter über alle Felder und Methoden hinweg:
| |
new Stack<number>() und new Stack<string>() sind beide vollständig typisiert, und die Klasse existiert nur einmal. Dasselbe Muster taucht überall auf, wo Sie Dependency Injection einsetzen: Sie schreiben ein Repository<T> einmal und injizieren dann Repository<User> und Repository<Order>, wo sie gebraucht werden, statt für jede Entität eine fast identische Klasse zu pflegen.
Mehrere Typparameter funktionieren genauso, und jeder kann seine eigene Einschränkung haben:
| |
Genauso bildet ein Result<T, E>-Typ mit zwei Parametern einen Erfolgswert und einen Fehlertyp nebeneinander ab. Wenn Sie entscheiden, ob Funktionen werfen oder Fehler als Daten zurückgeben sollen, behandelt Exceptions vs Error Values genau diese Entscheidung.
Wie funktionieren Standard-Typparameter?
Ein Typparameter kann einen Standardwert haben, genau wie ein normaler Funktionsparameter:
| |
Die Inferenz gewinnt weiterhin, wenn es ein Argument gibt, aus dem sich etwas herleiten lässt, wie raw zeigt. Der Standardwert greift, wenn nichts T füllt, etwa wenn Sie ApiResponse als Typannotation ohne Typargument schreiben. Das macht Standardwerte nützlich für Wrapper wie Response-Typen und Event-Payloads, bei denen sich viele Aufrufstellen nicht um den genauen Datentyp scheren. Nehmen Sie als Standard unknown, nicht any: Das erzwingt trotzdem eine Prüfung, bevor jemand data liest.
Wann sollten Sie einen Union Type statt eines Generics verwenden?
Stellen Sie sich eine Frage, bevor Sie <T> hinzufügen: Hängt der Rückgabetyp davon ab, was übergeben wurde? Gibt die Funktion unabhängig von der Eingabe immer denselben Typ zurück, reicht eine Union, und ein Generic bringt nichts:
| |
firstElement, getProperty und Stack<T> sind anders. Jede gibt etwas zurück, dessen Typ durch die Eingabe bestimmt wird, und genau diese Verbindung zwischen Eingabe und Ausgabe ist es, was ein Generic bringt. Lässt sich die Funktion mit einer Union schreiben, ohne etwas zu verlieren, nehmen Sie die Union.
Typparameter anzuhäufen, weil eine Funktion generisch sein könnte, kostet Lesbarkeit und bringt nichts. Ein Helper mit drei Typparametern, der nur mit string und number aufgerufen wird, ist schwerer zu lesen als zwei Überladungen. Fügen Sie <T> hinzu, wenn ein echter Aufrufer einen durchfließenden Typ braucht, und entfernen Sie es, wenn jede Aufrufstelle denselben konkreten Typ festlegt.
Warum können Sie T nicht zur Laufzeit verwenden?
Typparameter werden zur Compile-Zeit gelöscht. Das ausgeführte JavaScript enthält kein T, also können Sie keines konstruieren, instanceof T prüfen oder danach verzweigen:
| |
Muss eine Funktion ein T bauen oder zur Laufzeit untersuchen, übergeben Sie einen echten Wert als Argument: einen Konstruktor, ein Schema oder ein Typ-Tag. Generics sind ein Vertrag zur Compile-Zeit, keine Absicherung zur Laufzeit, also müssen Daten, die von außerhalb Ihres Programms kommen (eine API-Antwort, JSON.parse), weiterhin validiert werden, selbst wenn sie als ApiResponse<User> typisiert sind.
Um jeden Fehler aus dieser Anleitung selbst zu sehen, installieren Sie TypeScript mit npm, Yarn oder pnpm, je nachdem, was Ihr Projekt nutzt, fügen Sie die Beispiele in eine .ts-Datei ein und führen Sie npx tsc --noEmit --strict aus. Öffnen Sie dann das meistgenutzte any in Ihrem eigenen Code und prüfen Sie, ob ein <T> den Typ durchreichen würde.