1.6 Generics, any und unknown
Nach dieser Einheit kannst du:
- Generics wie
Array<T>undMap<K, V>lesen und erklären, wie eine generische Funktion wiefirstOrNull<T>den Typ weiterreicht. anyundunknownunterscheiden und Daten unbekannter Form durch eine Prüfung typisieren, statt sie mitanyoderasnur zu behaupten.- Utility-Types wie
Partial,OmitundPickals aus einem Typ berechnete Varianten lesen.
In Einheit 1.5 hat tsc als Kontrolle am Förderband jedes Etikett geprüft. Zwei Fälle hat das Bild offen gelassen: Kartons, bei denen erst beim Packen feststeht, was hineinkommt, und Pakete von außen, die gar kein Etikett tragen. Für den ersten Fall gibt es Generics, für den zweiten den Typ unknown.
flowchart LR
accTitle: Zwei offene Fälle für die Kontrolle am Förderband
box["Karton mit Leerfeld<br/>Inhalt: ___"] --> generic["Generic:<br/>Array#60;T#62;"]
parcel["Paket von außen<br/>ohne Etikett"] --> unknown["unknown:<br/>erst prüfen"]
Abb. 1.16: Für Kartons mit Leerfeld gibt es Generics, für Pakete ohne Etikett den Typ unknown (eigene Darstellung).
Beide begegnen dir das ganze Jahr: Generics in jedem Array und ab Kapitel 3 in jedem Promise, unknown an jeder Stelle, an der Daten von einem Server, aus einem Formular oder aus JSON.parse kommen. Am Ende steht ein Ausblick auf die Utility-Types, mit denen TypeScript neue Typen aus bestehenden berechnet.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Generic: ein Typ mit Platzhalter wie
T, der beim Verwenden durch einen echten Typ ersetzt wird, etwaArray<Clip>. - Typargument: der Typ, der für den Platzhalter eingesetzt wird, etwa
ClipinfirstOrNull<Clip>(clips). - Narrowing (Eingrenzen): Nach einer Prüfung wie
typeofkennt auch der Compiler einen engeren Typ. - Type Predicate: ein Rückgabetyp wie
value is Clip, der dem Compiler das Ergebnis einer Prüffunktion mitteilt. - Typzusicherung (
as): behauptet einen Typ, ohne ihn zu prüfen. - Utility-Type: ein eingebautes Generic, das aus einem Typ einen abgewandelten berechnet, etwa
Partial<Item>.
Generics: Typen mit Platzhalter
Abschnitt betitelt „Generics: Typen mit Platzhalter“Zurück in die Pakethalle: Ein Versandkarton ist immer gleich gebaut, nur das Feld „Inhalt: ___“ auf dem Etikett wird beim Packen ausgefüllt. Die Kontrolle am Förderband weiß danach bei jedem Karton, was drinnen ist. Ein Generic ist genau so ein Etikett mit Leerfeld: Array<T> ist der Karton, Array<Clip> der Karton mit dem Eintrag „Inhalt: Clip“. Die Grenze des Bildes: Das Leerfeld füllt meistens nicht du aus, sondern TypeScript selbst.
Das Problem ohne Generics
Abschnitt betitelt „Das Problem ohne Generics“Gesucht ist eine Funktion, die das erste Element einer Liste liefert, oder null, wenn die Liste leer ist. Ohne Generics gibt es zwei schlechte Wege:
// way 1: one copy per typefunction firstClip(items: Clip[]): Clip | null { return items[0] ?? null;}function firstTag(items: string[]): string | null { return items[0] ?? null;}
// way 2: one function for everything, but the type is lostfunction firstAny(items: any[]): any { return items[0] ?? null;}const lost = firstAny(clips); // type: anyconsole.log(lost.durationSecond); // no error, undefined at runtimeBeispielprojekt: 01_typescript_examples/ex18_generics
Weg 1 kopiert denselben Rumpf für jeden Typ; jede Korrektur muss in jede Kopie. Weg 2 ist nur eine Funktion, aber any wirft das Etikett weg: Was herauskommt, ist für tsc „irgendwas“, und der Tippfehler durationSecond aus Einheit 1.5 ist wieder unsichtbar. Ein Generic verbindet die Vorteile beider Wege:
| Frage | Kopie je Typ | any | Generic |
|---|---|---|---|
| Wie viele Funktionen? | eine pro Typ | eine | eine |
Kennt tsc den Typ des Ergebnisses? | ja | nein | ja |
Fällt durationSecond auf? | ja | nein | ja |
Die drei Stellen von T
Abschnitt betitelt „Die drei Stellen von T“function firstOrNull<T>(items: T[]): T | null { return items[0] ?? null;}
const clip = firstOrNull(clips); // type: Clip | nullconst tag = firstOrNull(["a", "b"]); // type: string | nullT taucht in der ersten Zeile dreimal auf, und jede Stelle hat eine eigene Aufgabe:
<T>hinter dem Funktionsnamen vereinbart den Platzhalter. Das ist eine Parameterliste für Typen: So wieitemsbeim Aufruf einen Wert bekommt, bekommtTbeim Aufruf einen Typ. Der NameT(für type) ist nur Konvention.items: T[]benutztTbeim Parameter. Hier erfährt TypeScript, wasTist: Werclipsübergibt, also einClip[], legtT = Clipfest.: T | nullbenutztTim Rückgabetyp. Hier kommt die Information wieder heraus: AusT | nullwirdClip | null.
flowchart LR
accTitle: TypeScript setzt T am Aufrufort ein
call["firstOrNull(clips)"] --> arg["clips hat den Typ<br/>Clip[]"]
arg --> bind["also gilt T = Clip"]
bind --> result["Rückgabe:<br/>Clip | null"]
Abb. 1.17: Am Aufrufort rechnet TypeScript T aus dem Argument aus und setzt es im Rückgabetyp ein (eigene Darstellung).
Konkret heißt das: Der Typ geht durch die Funktion hindurch, ohne verloren zu gehen. Wer danach clip?.durationSecond schreibt, bekommt wieder die vertraute Meldung Property 'durationSecond' does not exist on type 'Clip'. Did you mean 'durationSeconds'? (TS2551). Das ?. braucht es, weil clip auch null sein kann; ohne meldet tsc zusätzlich 'clip' is possibly 'null'. Im Rumpf weiß TypeScript über T dagegen nichts: items[0] hat den Typ T | undefined, weil ein leeres Array kein erstes Element hat, und ?? null macht daraus das versprochene T | null.
Inferenz oder ausdrückliches Typargument
Abschnitt betitelt „Inferenz oder ausdrückliches Typargument“const inferred = firstOrNull(clips); // T inferred: Clip | nullconst explicit = firstOrNull<Clip>(clips); // T given: Clip | nullconst empty = firstOrNull([]); // nothing to infer from: nullconst typedEmpty = firstOrNull<Clip>([]); // Clip | nullconst totals = new Map<string, number>(); // without <...>: Map<any, any>Meistens liest TypeScript T aus den Argumenten ab (Inferenz), und du schreibst keine spitzen Klammern. Das Typargument gibst du ausdrücklich an, wenn es nichts zum Ablesen gibt: bei einem leeren Array oder bei einer leeren Map. Ohne <string, number> wäre totals eine Map<any, any>, und das Etikett wäre wieder weg. Genau deshalb steht im vollständigen Beispiel aus Einheit 1.5 new Map<string, number>().
Generics, die du schon benutzt
Abschnitt betitelt „Generics, die du schon benutzt“Spitze Klammern hast du in diesem Kapitel schon mehrmals gesehen, nur ohne Namen dafür:
| Schreibweise | Bedeutung | Wo im Jahr |
|---|---|---|
Clip[], gleichwertig Array<Clip> | Array mit Elementen vom Typ Clip | ab jetzt ständig |
Map<string, number> | zwei Platzhalter: Typ der Schlüssel, Typ der Werte | Einheit 1.5, vollständiges Beispiel |
Partial<Clip> | Clip mit lauter optionalen Feldern | Abschnitt Utility-Types |
Promise<Response> | eine Response, die erst später ankommt | Kapitel 3 |
Wo begegnet dir das? Bei jeder Array-Methode aus Einheit 1.2: find, filter und map sind in TypeScript als Generics beschrieben. Deshalb hat clips.find((clip) => clip.durationSeconds > 60) den Typ Clip | undefined und nicht any. Ohne Generics müsste TypeScript für jede denkbare Elementart eine eigene find-Beschreibung mitliefern.
In diesem Jahr liest du Generics vor allem; selbst schreiben wirst du sie selten, nämlich dann, wenn eine Funktion für viele Typen gleich arbeitet und der Typ dabei nicht verloren gehen soll.
Auflösung
tsc meldet Argument of type 'Clip[]' is not assignable to parameter of type 'string[]' (TS2345). Durch <string> ist T = string festgelegt, an Stelle 1. An Stelle 2 wird daraus der Parametertyp string[], und clips ist ein Clip[]. Ein ausdrückliches Typargument ist also kein Befehl an TypeScript, sondern ein zusätzliches Etikett, das ebenfalls geprüft wird.
any und unknown: die Notausgänge des Typsystems
Abschnitt betitelt „any und unknown: die Notausgänge des Typsystems“Nach den Generics zwei Typen, die auf den ersten Blick dasselbe bedeuten - “der Typ steht nicht fest” - und die sich in der Praxis so unterschiedlich verhalten, dass an ihnen die Qualität eines TypeScript-Projekts hängt.
any: die Prüfung abschalten
Abschnitt betitelt „any: die Prüfung abschalten“any bedeutet nicht “irgendein Typ”, sondern “hör auf zu prüfen”. Auf einem any-Wert ist jeder Zugriff, jeder Aufruf und jede Zuweisung erlaubt:
const value: any = "not a number";
const total: number = value; // accepted - and wrongconsole.log(value.toFixed(2)); // accepted - crashes at runtimeconsole.log(value.whatever.x); // accepted - crashes at runtimeBeispielprojekt: 01_typescript_examples/ex20_any
Der Compiler schweigt zu allen drei Zeilen. Damit ist genau die Fehlerklasse zurück, gegen die TypeScript angetreten ist - und zwar unsichtbar, denn im Code steht ja ein Typ.
Verschärfend kommt hinzu: any ist ansteckend. Der Rückgabewert einer Funktion, die mit any arbeitet, ist wieder any, und so wandert der blinde Fleck durch das Programm. Ein einziges any an der falschen Stelle kann einen ganzen Modulpfad ungeprüft lassen.
unknown: der strenge Zwilling
Abschnitt betitelt „unknown: der strenge Zwilling“unknown sagt dasselbe über den Wert aus, zieht aber die umgekehrte Konsequenz: Solange nicht geklärt ist, was der Wert ist, ist nichts damit erlaubt.
const raw: unknown = JSON.parse(text);
const name = raw.filename; // error TS18046: 'raw' is of type 'unknown'Das ist keine Schikane, sondern die korrekte Aussage: Woher soll der Compiler wissen, dass in diesem JSON ein Feld filename steht? Der Weg zum Wert führt über eine Prüfung, und nach der Prüfung weiß auch der Compiler Bescheid (narrowing):
if (typeof raw === "object" && raw !== null && "filename" in raw) { console.log((raw as { filename: string }).filename);}Lesbarer wird das mit einer eigenen Prüffunktion, deren Rückgabetyp value is Clip dem Compiler das Ergebnis der Prüfung mitteilt (type predicate):
function isClip(value: unknown): value is Clip { return typeof value === "object" && value !== null && "filename" in value && typeof (value as Clip).filename === "string";}
if (isClip(raw)) { console.log(raw.filename.toUpperCase()); // raw is a Clip in here}Beispielprojekt: 01_typescript_examples/ex21_unknown_narrowing
Der Unterschied in einem Satz
Abschnitt betitelt „Der Unterschied in einem Satz“any | unknown | |
|---|---|---|
| Aussage | ”prüf nicht mehr" | "noch nicht geklärt” |
| Zugriff ohne Prüfung | erlaubt | Compile-Fehler |
| Zuweisung an andere Typen | überallhin | nur nach Prüfung |
| Wirkung auf den Rest | ansteckend | eingesperrt |
Deshalb gilt: An jeder Stelle, an der man any schreiben möchte, ist unknown die bessere erste Wahl.
Wo dir die beiden begegnen
Abschnitt betitelt „Wo dir die beiden begegnen“Wie weiß eine Wetter-App, dass die Antwort des Servers wirklich eine Temperatur enthält? Gar nicht, bis sie es prüft. Man sucht sich unknown deshalb selten aus - es kommt an den Rändern des Programms von selbst:
JSON.parse()liefert per Definition Daten unbekannter Form. Was ein Server schickt, weiß der Compiler nicht.catch (error)ist unter"strict": truevom Typunknown, denn in JavaScript kann alles geworfen werden, nicht nurError. Daher der Pflichtgriffif (error instanceof Error), bevorerror.messagegelesen wird.- Fremde Pakete ohne Typinformationen liefern
any; hier lohnt der Blick, ob es ein Paket@types/...gibt. - Formulardaten und HTTP-Anfragen kommen von außen und sind damit grundsätzlich unbekannt.
Genau hier liegt die Brücke zum Rest des Jahres: Die Frage “wie wird aus unbekannten Daten ein geprüfter, typisierter Wert?” beantwortet Kapitel 12 mit Schema-Validierung - ein Werkzeug, das die Prüfung schreibt und den passenden Typ gleich mitliefert.
Die Regeln fürs Jahr
Abschnitt betitelt „Die Regeln fürs Jahr“-
Standard ist
unknown, wenn der Typ tatsächlich offen ist - besonders an den Grenzen zu fremden Daten. -
Prüfen statt behaupten: aus
unknownwird ein brauchbarer Typ durchtypeof,instanceof,inoder eine eigene Prüffunktion. -
anynur als bewusste Notlösung, und dann mit einem Kommentar, der begründet, warum hier nicht geprüft wird. Ein unkommentiertesanyist im Review eine Anmerkung wert. -
Nie
anygegen eine Fehlermeldung einsetzen. Der Compiler meldet an dieser Stelle etwas Richtiges;anylöscht die Meldung, nicht das Problem.
Auflösung
Mit unknown meldet tsc sofort error TS18046: 'value' is of type 'unknown'. Akzeptiert wird der Aufruf erst nach einer Prüfung: if (typeof value === "number") { console.log(value.toFixed(2)); }. Mit dem String aus dem Beispiel läuft der Zweig gar nicht, statt zur Laufzeit abzustürzen.
Utility-Types: Typen aus Typen bauen
Abschnitt betitelt „Utility-Types: Typen aus Typen bauen“Bisher haben wir Typen von Hand geschrieben. TypeScript kann Typen aber auch aus anderen Typen berechnen. Die dafür mitgelieferten Bausteine heißen Utility-Types: eingebaute Generics, die einen bestehenden Typ nehmen und einen abgewandelten zurückgeben. Der Vorteil ist immer derselbe - es gibt eine Quelle der Wahrheit, und die Abwandlungen bleiben automatisch synchron.
Als Ausgangspunkt dient ein Datenvertrag, wie er uns im Jahr ständig begegnen wird:
interface Item { id: number; name: string; price: number; // in cents available: number;}Partial<T> macht alle Felder optional - passend für ein Update, das nur einzelne Felder ändert:
// { id?: number; name?: string; price?: number; available?: number }function updateItem(id: number, changes: Partial<Item>): void { /* ... */ }
updateItem(1, { price: 350 }); // only the price, the rest stays untouchedOmit<T, K> entfernt Felder, Pick<T, K> behält nur die genannten. Ein neues Item hat noch keine id - die vergibt der Server:
type NewItem = Omit<Item, "id">; // everything except idtype ItemPreview = Pick<Item, "id" | "name">; // only id and nameReadonly<T> friert alle Felder ein; eine spätere Zuweisung ist dann schon zur Compile-Zeit ein Fehler:
const frozen: Readonly<Item> = { id: 1, name: "Cola", price: 200, available: 5 };frozen.price = 250; // error: cannot assign to 'price' because it is read-onlyRecord<K, V> beschreibt ein Objekt, das Schlüssel auf Werte abbildet - nützlich für Nachschlagetabellen:
// a lookup from item id to its ordered quantityconst quantities: Record<number, number> = { 1: 2, 4: 1 };Beispielprojekt: 01_typescript_examples/ex22_utility_types
Diese Bausteine lassen sich kombinieren (Partial<Omit<Item, "id">> ist “alle Felder außer id, und die optional”) - genau diese Kombination werden wir im Kapitel Mehrschichtarchitektur für Repository-Updates wiedersehen.
Wo begegnet dir das? In jedem Formular “Profil bearbeiten”: Geschickt werden nur die Felder, die du geändert hast, also ein Partial des Profils. Beim Registrieren gibt es dagegen noch keine Benutzer-ID, die vergibt erst der Server, also ein Omit<User, "id">.
Das folgende Video führt die wichtigsten Utility-Types an Beispielen vor:
Stolperfallen
Abschnitt betitelt „Stolperfallen“anygegen eine rote Fehlermeldung: Die Meldung verschwindet, der Fehler bleibt. Stattdessen die Meldung lesen und die Ursache beheben (oderunknownplus Prüfung).asohne vorangehende Prüfung: Eine Typzusicherung prüft nichts. Stimmt die Behauptung nicht, knallt es erst zur Laufzeit.- Leere
Mapohne Typargument:new Map()ist eineMap<any, any>, und das Etikett ist weg. Bei leeren Sammlungen gehört das Typargument dazu:new Map<string, number>(). - Ausdrückliches Typargument als Befehl verstanden:
firstOrNull<string>(clips)zwingt TypeScript nichts auf, sondern wird ebenfalls geprüft und ergibt hier TS2345. error.messageimcatchohne Prüfung: Unter"strict": trueisterrorvom Typunknown; erstif (error instanceof Error)machterror.messagezugänglich.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- TypeScript-Handbuch: Generics - vom Typparameter bis zu Einschränkungen mit
extends, die du in diesem Jahr nur lesen musst. - TypeScript-Handbuch: Narrowing - wie
typeof,in,instanceofund eigene Prüffunktionen einen Typ Schritt für Schritt eingrenzen. - TypeScript-Handbuch: Utility Types - vollständige Liste von
Partial,Omit,Pick,Recordund Co., falls du den Ausblick vertiefen willst. - TypeScript Playground - Generics und Narrowing im Browser ausprobieren; beim Darüberfahren mit der Maus zeigt der Playground den errechneten Typ.
Zum Anhören
Abschnitt betitelt „Zum Anhören“Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“Du kannst jetzt:
- Generics wie
Array<T>undMap<K, V>lesen und erklären, wie eine generische Funktion wiefirstOrNull<T>den Typ weiterreicht. anyundunknownunterscheiden und Daten unbekannter Form durch eine Prüfung typisieren, statt sie mitanyoderasnur zu behaupten.- Utility-Types wie
Partial,OmitundPickals aus einem Typ berechnete Varianten lesen.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 02 - Typen retten den Tag, Stufe MandalorianPlus (
any-Gegenprobe, PrüffunktionisStudent) - Aufgabe 03 - TypeScript-Katas, Stufe PlusPlus (
parseAppointmentmitunknown)
Quellen
Abschnitt betitelt „Quellen“- Generics (
firstOrNull<T>, Typargument,Array<T>,Map<K, V>) → TypeScript-Handbuch: Generics;Map<any, any>beinew Map(), TS2345 beifirstOrNull<string>(clips), TS2551 undClip | undefinedbeifind→ eigener Test mit TypeScript 7.0.2 (Oktober 2026) unknown, Narrowing und type predicates → TypeScript-Handbuch: Narrowing- Utility-Types (
Partial,Omit,Pick,Readonly,Record) → TypeScript-Handbuch: Utility Types - TypeScript allgemein → TypeScript-Handbuch, TypeScript Playground
- Video zu den Utility-Types → „TypeScript’s Utility Types… Blazing fast”, Matt Pocock, eingebettet, nicht kopiert
- Abb. 1.16 (zwei offene Fälle, Mermaid-Diagramm), Abb. 1.17 (
Tam Aufrufort, Mermaid-Diagramm), Abb. 1.19 (anyundunknown), Abb. 1.20 (Utility-TypesPartial,Omit,Pick) → eigene Darstellung - Abb. 1.18 (Meme zum
any-Typ,assets/01/any_typescript.jpg) → Lizenz unbekannt (Bestandsbild)
Autoren
Abschnitt betitelt „Autoren“- Oliver Maurhart (MAO)
- Michael Gabriel (GAM)
CC-BY-SA