Zum Inhalt springen

1.6 Generics, any und unknown

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • Generics wie Array<T> und Map<K, V> lesen und erklären, wie eine generische Funktion wie firstOrNull<T> den Typ weiterreicht.
  • any und unknown unterscheiden und Daten unbekannter Form durch eine Prüfung typisieren, statt sie mit any oder as nur zu behaupten.
  • Utility-Types wie Partial, Omit und Pick als 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.

  • Generic: ein Typ mit Platzhalter wie T, der beim Verwenden durch einen echten Typ ersetzt wird, etwa Array<Clip>.
  • Typargument: der Typ, der für den Platzhalter eingesetzt wird, etwa Clip in firstOrNull<Clip>(clips).
  • Narrowing (Eingrenzen): Nach einer Prüfung wie typeof kennt 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>.

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.

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 type
function 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 lost
function firstAny(items: any[]): any {
return items[0] ?? null;
}
const lost = firstAny(clips); // type: any
console.log(lost.durationSecond); // no error, undefined at runtime

Beispielprojekt: 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:

FrageKopie je TypanyGeneric
Wie viele Funktionen?eine pro Typeineeine
Kennt tsc den Typ des Ergebnisses?janeinja
Fällt durationSecond auf?janeinja
function firstOrNull<T>(items: T[]): T | null {
return items[0] ?? null;
}
const clip = firstOrNull(clips); // type: Clip | null
const tag = firstOrNull(["a", "b"]); // type: string | null

T taucht in der ersten Zeile dreimal auf, und jede Stelle hat eine eigene Aufgabe:

  1. <T> hinter dem Funktionsnamen vereinbart den Platzhalter. Das ist eine Parameterliste für Typen: So wie items beim Aufruf einen Wert bekommt, bekommt T beim Aufruf einen Typ. Der Name T (für type) ist nur Konvention.
  2. items: T[] benutzt T beim Parameter. Hier erfährt TypeScript, was T ist: Wer clips übergibt, also ein Clip[], legt T = Clip fest.
  3. : T | null benutzt T im Rückgabetyp. Hier kommt die Information wieder heraus: Aus T | null wird Clip | 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.

const inferred = firstOrNull(clips); // T inferred: Clip | null
const explicit = firstOrNull<Clip>(clips); // T given: Clip | null
const empty = firstOrNull([]); // nothing to infer from: null
const typedEmpty = firstOrNull<Clip>([]); // Clip | null
const 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>().

Spitze Klammern hast du in diesem Kapitel schon mehrmals gesehen, nur ohne Namen dafür:

SchreibweiseBedeutungWo im Jahr
Clip[], gleichwertig Array<Clip>Array mit Elementen vom Typ Clipab jetzt ständig
Map<string, number>zwei Platzhalter: Typ der Schlüssel, Typ der WerteEinheit 1.5, vollständiges Beispiel
Partial<Clip>Clip mit lauter optionalen FeldernAbschnitt Utility-Types
Promise<Response>eine Response, die erst später ankommtKapitel 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.

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 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 wrong
console.log(value.toFixed(2)); // accepted - crashes at runtime
console.log(value.whatever.x); // accepted - crashes at runtime

Beispielprojekt: 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.

Der any-Typ in TypeScript: ein Typ, der alle Werte akzeptiert
Abb. 1.18: Der any-Typ in TypeScript. Wenn du Compiler-Fehler mit any 'löst', statt die Ursache zu beheben, sind sehr oft Fehler und Probleme zu erwarten.

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

anyunknown
Aussage”prüf nicht mehr""noch nicht geklärt”
Zugriff ohne PrüfungerlaubtCompile-Fehler
Zuweisung an andere Typenüberallhinnur nach Prüfung
Wirkung auf den Restansteckendeingesperrt
Oben: Daten von außen als any gehen ohne Prüfung direkt in den Code, value.toFixed(2) wird akzeptiert und stürzt zur Laufzeit ab. Unten: Daten von außen als unknown müssen erst durch eine Prüfung mit typeof, in oder einer Prüffunktion wie isClip; danach sind sie ein Clip, was die Prüfung nicht besteht, wird abgelehnt
Abb. 1.19: any lässt alles ungeprüft durch, unknown verlangt zuerst eine Prüfung (eigene Darstellung).

Deshalb gilt: An jeder Stelle, an der man any schreiben möchte, ist unknown die bessere erste Wahl.

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": true vom Typ unknown, denn in JavaScript kann alles geworfen werden, nicht nur Error. Daher der Pflichtgriff if (error instanceof Error), bevor error.message gelesen 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.

  1. Standard ist unknown, wenn der Typ tatsächlich offen ist - besonders an den Grenzen zu fremden Daten.

  2. Prüfen statt behaupten: aus unknown wird ein brauchbarer Typ durch typeof, instanceof, in oder eine eigene Prüffunktion.

  3. any nur als bewusste Notlösung, und dann mit einem Kommentar, der begründet, warum hier nicht geprüft wird. Ein unkommentiertes any ist im Review eine Anmerkung wert.

  4. Nie any gegen eine Fehlermeldung einsetzen. Der Compiler meldet an dieser Stelle etwas Richtiges; any lö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.

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 untouched

Omit<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 id
type ItemPreview = Pick<Item, "id" | "name">; // only id and name

Readonly<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-only

Record<K, V> beschreibt ein Objekt, das Schlüssel auf Werte abbildet - nützlich für Nachschlagetabellen:

// a lookup from item id to its ordered quantity
const quantities: Record<number, number> = { 1: 2, 4: 1 };

Beispielprojekt: 01_typescript_examples/ex22_utility_types

Aus dem Interface Item mit id, name, price und available entstehen drei Typen: Partial mit allen Feldern optional, Omit ohne id mit name, price und available, Pick mit nur id und name
Abb. 1.20: Utility-Types berechnen neue Typen aus einem bestehenden (eigene Darstellung).

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:

Abspielen
  1. any gegen eine rote Fehlermeldung: Die Meldung verschwindet, der Fehler bleibt. Stattdessen die Meldung lesen und die Ursache beheben (oder unknown plus Prüfung).
  2. as ohne vorangehende Prüfung: Eine Typzusicherung prüft nichts. Stimmt die Behauptung nicht, knallt es erst zur Laufzeit.
  3. Leere Map ohne Typargument: new Map() ist eine Map<any, any>, und das Etikett ist weg. Bei leeren Sammlungen gehört das Typargument dazu: new Map<string, number>().
  4. Ausdrückliches Typargument als Befehl verstanden: firstOrNull<string>(clips) zwingt TypeScript nichts auf, sondern wird ebenfalls geprüft und ergibt hier TS2345.
  5. error.message im catch ohne Prüfung: Unter "strict": true ist error vom Typ unknown; erst if (error instanceof Error) macht error.message zugänglich.
  • 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, instanceof und eigene Prüffunktionen einen Typ Schritt für Schritt eingrenzen.
  • TypeScript-Handbuch: Utility Types - vollständige Liste von Partial, Omit, Pick, Record und 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.

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.

Du kannst jetzt:

  • Generics wie Array<T> und Map<K, V> lesen und erklären, wie eine generische Funktion wie firstOrNull<T> den Typ weiterreicht.
  • any und unknown unterscheiden und Daten unbekannter Form durch eine Prüfung typisieren, statt sie mit any oder as nur zu behaupten.
  • Utility-Types wie Partial, Omit und Pick als aus einem Typ berechnete Varianten lesen.
  • Aufgabe 02 - Typen retten den Tag, Stufe MandalorianPlus (any-Gegenprobe, Prüffunktion isStudent)
  • Aufgabe 03 - TypeScript-Katas, Stufe PlusPlus (parseAppointment mit unknown)
  • Generics (firstOrNull<T>, Typargument, Array<T>, Map<K, V>) → TypeScript-Handbuch: Generics; Map<any, any> bei new Map(), TS2345 bei firstOrNull<string>(clips), TS2551 und Clip | undefined bei find → 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 (T am Aufrufort, Mermaid-Diagramm), Abb. 1.19 (any und unknown), Abb. 1.20 (Utility-Types Partial, Omit, Pick) → eigene Darstellung
  • Abb. 1.18 (Meme zum any-Typ, assets/01/any_typescript.jpg) → Lizenz unbekannt (Bestandsbild)
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules