Typen weiterreichen

Generics: ein Platzhalter, der Information transportiert

Der Compiler füllt den Platzhalter beim Aufruf mit dem tatsächlichen Typ. Das lohnt sich, sobald er zweimal in der Signatur steht, und stört, sobald er nur einmal auftaucht.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Ohne Typparameter verlierst du die Information genau an der Grenze der Funktion

Der Klassiker ist eine kleine Hilfsfunktion, die aus einer Liste den ersten Treffer zu einer Kennung holt. Ohne Typparameter hast du zwei schlechte Möglichkeiten. Entweder du schreibst die Signatur auf einen konkreten Typ, dann brauchst du dieselbe Funktion für Kunden, Rechnungen und Termine noch einmal. Oder du nimmst any oder ein sehr weites Objekt, dann funktioniert sie überall, und der Aufrufer bekommt einen Wert zurück, mit dem der Compiler nichts mehr anfangen kann.

Die zweite Variante fühlt sich pragmatisch an und ist die teurere. Der Rückgabewert wandert danach durch den Code, und jede Stelle, die etwas mit ihm macht, ist ungeprüft. Wer ihn wieder brauchbar machen will, schreibt eine Zusicherung an den Aufrufort, also as Kunde. Damit steht dieselbe Behauptung an fünf Stellen statt der Wahrheit an einer, und wenn sich der Datentyp ändert, findet der Compiler keine davon.

Genauso teuer ist der umgekehrte Fehler. Eine Funktion mit vier Typparametern, von denen jeder nur einmal auftaucht, sieht anspruchsvoll aus und leistet nichts. Beim Hovern erscheint eine Signatur über vier Zeilen, die Fehlermeldungen bei einem falschen Argument werden entsprechend lang, und niemand im Team traut sich mehr, die Funktion zu ändern. Der Unterschied zwischen nützlich und lästig liegt nicht in der Menge, sondern in der Frage, ob der Platzhalter tatsächlich etwas von einer Stelle zur anderen trägt.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Aufbau im Detail

Der Aufbau im Detail

Der Aufbau

function ersterTreffer<T extends HatKennung>(liste: T[], kennung: string): T | undefined
  1. 01 Eine ganz gewöhnliche Funktion function ersterTreffer

    An der Funktion selbst ist nichts Besonderes. Generics sind kein eigenes Sprachkonstrukt, sondern eine Ergänzung der Signatur. Alles, was du über Parameter und Rückgabewerte weißt, gilt unverändert weiter.

  2. 02 Der Typparameter mit seiner Einschränkung <T extends HatKennung>

    T ist der Platzhalter, den der Compiler beim Aufruf füllt. extends HatKennung verlangt, dass der eingesetzte Typ mindestens die Felder dieses Typs hat. Ohne diese Einschränkung dürfte der Rumpf gar nicht auf ein Feld zugreifen, weil T sonst auch eine Zahl sein könnte.

  3. 03 Die Stelle, aus der abgeleitet wird liste: T[]

    Aus diesem Argument liest der Compiler, was T tatsächlich ist. Übergibst du eine Liste von Kunden, ist T ab dieser Zeile Kunde. Genau deshalb brauchst du beim Aufruf keine spitzen Klammern zu schreiben.

  4. 04 Ein Parameter ohne Beteiligung kennung: string

    Nicht jeder Parameter muss am Typspiel teilnehmen. Die Kennung ist immer eine Zeichenkette, unabhängig davon, was in der Liste steht, und bekommt deshalb einen ganz normalen Typ.

  5. 05 Die zweite Fundstelle des Platzhalters T | undefined

    Hier zahlt sich der Aufwand aus: Der Aufrufer bekommt genau den Typ zurück, den er hineingegeben hat, ohne Zusicherung und ohne Umweg. Das undefined daneben ist kein Zubehör, sondern die ehrliche Antwort auf die Frage, was bei null Treffern herauskommt.

Wenn es nicht funktioniert

Das siehst du

Property 'kennung' does not exist on type 'T'.

Warum

Der Rumpf greift auf ein Feld zu, aber T ist unbeschränkt. Für den Compiler kann T alles sein, auch eine Zahl oder null, und deshalb kennt er dieses Feld nicht.

Was hilft

Schränk mit extends ein, und zwar genau auf das, was der Rumpf braucht: <T extends { kennung: string }> oder ein benannter Typ dafür. Eine Zusicherung im Rumpf löst die Meldung ebenfalls auf, verlagert das Problem aber zum Aufrufer.

Das siehst du

Der Aufruf verlangt plötzlich, dass du den Typ in spitzen Klammern angibst, sonst wird alles zu unknown.

Warum

Der Platzhalter kommt nur im Rückgabetyp vor. Der Compiler hat keine Stelle, aus der er ableiten könnte, und nimmt deshalb die Einschränkung oder unknown.

Was hilft

Prüf, ob der Typparameter überhaupt einen Zweck hat. Meist ist die ehrliche Signatur eine mit unknown als Rückgabetyp, sodass der Aufrufer einmal ausdrücklich prüft, statt eine Behauptung geschenkt zu bekommen.

Das siehst du

Nach einem Aufruf mit leerer Liste meldet der Compiler beim Zugriff auf das Ergebnis: Property 'kennung' does not exist on type 'never'.

Warum

Aus einem leeren Array lässt sich nichts ableiten. Der Compiler setzt für T deshalb never ein, der Rückgabetyp wird zu never oder undefined, und damit ist jeder Zugriff auf das Ergebnis ungültig.

Was hilft

Gib den Typ an dieser einen Stelle ausdrücklich an, also ersterTreffer<Kunde>([], 'K-1'), oder leg die leere Liste vorher typisiert an: const liste: Kunde[] = []. Ein Standardwert am Typparameter erledigt denselben Fall für alle Aufrufer.

Das siehst du

Beim Hovern erscheint eine Signatur über vier Zeilen, und die Fehlermeldung bei einem falschen Argument ist noch länger.

Warum

Zu viele Typparameter, meist entstanden, weil jeder neue Anwendungsfall einen weiteren bekommen hat. Häufig taucht ein Teil davon nur einmal auf und trägt gar keine Information.

Was hilft

Streich jeden Platzhalter, der nur einmal in der Signatur steht. Für die verbleibenden vergibst du sprechende Namen statt T, U und V, und Zwischenergebnisse bekommen einen eigenen benannten Typ.

Das siehst du

Der abgeleitete Typ ist string, obwohl im Aufruf eine feste Zeichenkette stand und du genau diese gebraucht hättest.

Warum

Der Compiler verallgemeinert Literale, sobald sie nicht in einer festen Position stehen. Aus 'aktiv' wird string, und die engere Information ist weg.

Was hilft

Nimm einen const-Typparameter, also <const T>, oder häng as const an das Argument. Beides hält die Literalform fest, ohne dass der Aufrufer die Typen ausschreiben muss.

Fünf Fragen, und der Typparameter steht fest

  1. 01 Hängt der Typ des Ergebnisses vom Typ der Argumente ab? Sonst kein Generic.
  2. 02 Kommt der Platzhalter zweimal in der Signatur vor? Sonst ist er überflüssig.
  3. 03 Greift der Rumpf auf ein Feld zu? Dann braucht er eine Einschränkung mit extends.
  4. 04 Geht es um einen Feldnamen? Dann nimm K extends keyof T und den Typ T[K].
  5. 05 Musst du beim Aufruf spitze Klammern setzen? Dann stimmt der Entwurf nicht.
Was du mitnimmst

Was du danach beim Entwurf einer Signatur entscheidest

Generics sind kein eigenes Kapitel der Sprache, sondern eine Antwort auf eine bestimmte Frage: Hängt der Typ des Ergebnisses vom Typ der Eingabe ab? Wo diese Frage mit Ja beantwortet wird, gehört ein Typparameter hin, und sonst nicht.

Die Zweimal-Regel als Prüfung

Du siehst dir an, wie oft der Platzhalter in der Signatur vorkommt. Steht er nur einmal, ersetzt du ihn durch den konkreten Typ oder durch unknown, denn er verbindet nichts miteinander und macht die Signatur nur länger.

Ableiten lassen statt angeben

Du rufst generische Funktionen ohne spitze Klammern auf und lässt den Compiler den Typ aus den Argumenten schließen. Wenn du die Klammern setzen musst, ist das ein Hinweis auf einen Entwurfsfehler und nicht auf fehlende Sorgfalt.

Mit extends nur so weit einschränken wie nötig

Du schränkst genau auf das ein, was der Rumpf wirklich braucht. Eine Einschränkung auf { id: string } erlaubt jede Struktur mit einer Kennung, eine auf einen konkreten Datentyp schließt alle anderen aus, ohne dass es einen Grund gäbe.

keyof für Zugriffe auf Feldnamen

Mit K extends keyof T und dem Rückgabetyp T[K] baust du Zugriffsfunktionen, bei denen ein Tippfehler im Feldnamen auffällt und der Rückgabetyp automatisch stimmt. Das ist der häufigste sinnvolle Fall für zwei Parameter.

Standardwerte für Typparameter setzen

Ein Standard wie T = unknown erspart Aufrufern die Angabe im Normalfall und lässt sie trotzdem zu, wenn es genauer sein soll. Das ist besonders bei Datentypen für Antworten nützlich, die meist gleich aussehen.

Lesbarkeit als Abbruchkriterium

Wenn du die Signatur beim Vorlesen nicht in einem Satz erklären kannst, teilst du die Funktion auf oder benennst einen Zwischentyp. Eine Signatur, die niemand liest, prüft am Ende auch niemand.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Was der Platzhalter genau leistet

Ein Typparameter ist keine Prüfung und kein Umwandlungsschritt, sondern reine Buchführung des Compilers. Beim Aufruf sieht er sich die Argumente an, füllt den Platzhalter und rechnet mit dem Ergebnis für den Rest des Ausdrucks weiter. Zur Laufzeit bleibt davon nichts übrig, das übersetzte JavaScript kennt keine Typparameter. Das ist wichtig für die Erwartungshaltung: Ein Generic kann nichts prüfen, was erst zur Laufzeit feststeht, und eignet sich deshalb nicht als Ersatz für eine Prüfung an der Systemgrenze.

Aus derselben Eigenschaft folgt eine Regel für die Praxis, die sich einfach anwenden lässt: Ein Typparameter, der in der Signatur nur einmal vorkommt, verbindet nichts. Er nimmt einen Typ entgegen und tut nichts damit. Solche Stellen lassen sich fast immer durch die Einschränkung selbst ersetzen, also statt <T extends HatKennung>(wert: T): void einfach (wert: HatKennung): void. Die Signatur wird kürzer, die Fehlermeldungen ebenfalls, und es geht keine Information verloren.

extends ist eine Bedingung, kein Vererbungsbegriff

In <T extends HatKennung> heißt extends nicht, dass eine Klasse von einer anderen erbt. Es heißt: Was für T eingesetzt wird, muss zu HatKennung passen. Eine Struktur mit den Feldern kennung, name und ort erfüllt eine Einschränkung auf { kennung: string } ohne Weiteres, denn TypeScript vergleicht Formen und keine Abstammung. Genau das macht die Einschränkung im Alltag nützlich: Sie beschreibt eine Anforderung, keine Zugehörigkeit.

Die häufigste sinnvolle Einschränkung ist keyof. Mit function feld<T, K extends keyof T>(objekt: T, name: K): T[K] baust du einen Zugriff, bei dem ein Tippfehler im Feldnamen sofort auffällt und der Rückgabetyp automatisch der richtige ist. Ruf ihn mit einem Kunden und 'name' auf, kommt string zurück, ruf ihn mit 'umsatz' auf, kommt number zurück. Für diese Art von Funktionen sind Generics gebaut, und hier steht der Aufwand in einem klaren Verhältnis zum Nutzen.

Ein Detail, das regelmäßig überrascht: Eine Einschränkung schließt nicht aus, dass mehr da ist. Wer <T extends { kennung: string }> schreibt und im Rumpf ein Objekt mit genau diesem einen Feld zurückgibt, bekommt eine Meldung, denn T könnte auch die Felder name und ort enthalten, und die fehlen im Ergebnis. Der Compiler hat recht: Der Aufrufer erwartet seinen vollständigen Typ zurück. Baust du solche Ergebnisse im Rumpf zusammen, gehört meist ein Spread über das ursprüngliche Objekt hinein.

Standardwerte und wann sie wirklich helfen

Ein Typparameter darf einen Standard bekommen, geschrieben als <T = unknown>. Sinnvoll ist das überall dort, wo der übliche Fall immer gleich aussieht und nur wenige Aufrufer es genauer wissen wollen. Ein typischer Ort ist eine eigene Funktion für Netzabfragen: Die meisten Aufrufer interessiert der Aufbau der Antwort nicht, ein paar wollen ihn benennen. Wichtig ist die Wahl des Standards. unknown zwingt zu einer Prüfung, any tut es nicht. Ein Standard von any macht aus einer bequemen Funktion eine, die im ganzen Projekt die Prüfung ausschaltet, und zwar unauffällig, weil an den Aufrufstellen nichts Verdächtiges steht.

Nicht verwechseln solltest du den Standard mit der Einschränkung. <T extends Basis = Basis> hat beides: T muss zu Basis passen, und wenn nichts angegeben ist, ist es genau Basis. Wer nur den Standard setzt, erlaubt weiterhin jeden Typ. Und wer nur die Einschränkung setzt, bekommt ohne ableitbares Argument stillschweigend die Einschränkung selbst eingesetzt, also den weitesten erlaubten Typ, und an der Aufrufstelle sieht das niemand.

Wo Generics im Alltag ohnehin schon vorkommen

Viele Signaturen, mit denen du täglich arbeitest, sind generisch, ohne dass es auffällt. Array<T>, Promise<T>, Map<K, V> und Record<K, T> sind Beispiele, ebenso useState in React: Aus useState('') schließt der Compiler string, und aus useState<string[]>([]) machst du eine Liste, die sonst als never[] geraten worden wäre. Genau dieser zweite Fall ist der übliche Grund, warum man doch einmal spitze Klammern schreibt, und er ist berechtigt: Aus einem leeren Startwert lässt sich nichts ableiten.

Beim Lesen fremder Bibliotheken hilft dieselbe Prüfung wie beim eigenen Entwurf. Sieh nach, wo der Platzhalter zum zweiten Mal auftaucht. Steht er einmal im Argument und einmal im Rückgabewert, weißt du sofort, dass die Funktion deinen Typ durchreicht. Steht er in einer Einschränkung mit keyof, weißt du, dass es um Feldnamen geht. Damit ist die Hälfte der Signaturen, die auf den ersten Blick unlesbar wirken, in zehn Sekunden eingeordnet, ohne dass du die Dokumentation aufschlagen musst.

Dazu passende Kurse

Wie du einen Typparameter durch eine Funktion tragen kannst, ohne dass die Signatur unlesbar wird, gehört zum Stoff der TypeScript-Kurse.

Weil die Frage nach der richtigen Signatur älter ist als TypeScript, passen dazu auch Kurse zu sauberem Schnittstellenentwurf .

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Ein sehr gutes, praxisorientiertes und nachhaltiges Seminar. So sollte es immer sein. Vielen Dank.
LimeSurvey - Anwendertraining Teil 1 (Grundlagen)
Die Schulung ist gerade für Einsteiger oder Entwickler*innen mit eingestaubtem Basiswissen sehr hilfreich.
Vue.js 3 Grundkurs
Ich bin sehr glücklich Franz als Trainer gehabt zu haben. Er hat offensichtlich unglaublich viel Wissen zu den Themen, und schafft es, dieses auch interaktiv und verständlich weiterzugeben.
React Komplettausbildung

Häufige Fragen

Wann nehme ich unknown statt eines Typparameters?
Immer dann, wenn der Typ des Ergebnisses nicht vom Typ der Eingabe abhängt. Eine Funktion, die etwas entgegennimmt und protokolliert, braucht keinen Platzhalter, denn sie gibt nichts zurück, was mit der Eingabe zu tun hätte. Der Test ist simpel: Steht der Platzhalter nur an einer Stelle in der Signatur, ist unknown die ehrlichere und kürzere Fassung.
Warum heißt der Typparameter immer T?
Das ist reine Gewohnheit aus anderen Sprachen und bei einer einzigen Signaturzeile auch völlig in Ordnung. Sobald zwei oder drei Platzhalter im Spiel sind, wird es unlesbar. Sprechende Namen wie TEintrag und TSchluessel kosten nichts und ersparen beim Lesen einer fremden Signatur das Rückwärtssuchen, wofür der jeweilige Buchstabe steht.
Kann ich einen Typparameter zur Laufzeit auslesen?
Nein. Typparameter existieren nur während der Übersetzung, im erzeugten JavaScript kommen sie nicht vor. Wenn du zur Laufzeit wissen musst, was für ein Wert vorliegt, brauchst du eine echte Prüfung, also typeof, instanceof, ein Feld im Objekt, das die Art angibt, oder ein Schema. Ein Generic kann diese Prüfung nicht ersetzen, es kann ihr Ergebnis nur weitertragen.
Was mache ich, wenn die Ableitung einen zu weiten Typ liefert?
Das passiert regelmäßig bei festen Zeichenketten, aus denen der Compiler string macht. Zwei Wege helfen: as const am Argument hält die Literalform fest, und ein const-Typparameter, geschrieben als , erledigt dasselbe von der Signaturseite aus, sodass die Aufrufer nichts dazuschreiben müssen. Der zweite Weg ist der freundlichere, wenn die Funktion oft benutzt wird.
Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Webentwicklung-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

Signaturen, die im halben Jahr noch jemand liest

In den TypeScript-Kursen bei cmt entwirfst du generische Funktionen an eigenen Fällen und siehst dabei, ab welchem Punkt eine Signatur mehr kostet, als sie einbringt.