TypeScript Typing 2026: Interfaces, Generics und Utility Types in der Praxis

TypeScript Typing 2026: Interfaces, Generics und Utility Types in der Praxis

TypeScript Typing ist die Kunst, dem Compiler genau so viel über Ihre Daten zu sagen, dass er Fehler findet, die Sie sonst erst im Produktivbetrieb sehen — und nicht so viel, dass Sie den halben Tag mit dem Typsystem ringen statt Software zu bauen. Zwischen diesen beiden Extremen liegt die ganze Praxis.

Dieser Artikel behandelt die konkreten Werkzeuge: Interfaces und Types, Generics, Utility Types, unknown gegen any, Type Guards und Mapped Types. Und er behandelt die eine Sache, die in den meisten Tutorials fehlt, obwohl sie die häufigste Ursache für Produktionsfehler in typisierten Projekten ist: an der Systemgrenze hört Ihr Typsystem auf zu schützen — und der Compiler sagt Ihnen kein Wort davon.

Jede Fehlermeldung in diesem Artikel ist echt. Wir haben jedes Beispiel in einem frischen Projekt mit TypeScript 7.0.2 kompiliert und die Ausgabe im Wortlaut übernommen, samt Fehlernummer. Wo Code zur Laufzeit abstürzt, haben wir ihn abstürzen lassen und den Stacktrace mitgenommen.

Messstand dieses Artikels: 5. August 2026

WasWertWie geprüft
TypeScript stabil7.0.2npm view typescript dist-tags
TypeScript Vorabversion7.1.0-dev.20260804.1dito (next-Tag)
Zod (für den Laufzeitteil)4.4.3npm view zod version
Node für Laufzeit-Beweisev22.23.0node --version
Compiler-Optionenstrict: true, noEmit, Target ES2022siehe tsconfig.json unten

Alle Beispiele liefen gegen diese tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "preserve",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}

Wenn Sie dieselben Beispiele ohne strict: true ausprobieren, bekommen Sie weniger Fehler — und ein falsches Gefühl von Sicherheit. Dazu weiter unten mehr.

Die kurze Version für Eilige

  • interface und type sind zu 95 % austauschbar. Der einzige harte Unterschied: Interfaces lassen sich nachträglich erweitern (Declaration Merging), Types nicht. Für öffentliche APIs ist das ein Feature, für interne Datenformen eher ein Risiko.
  • Generics sind Platzhalter, keine Magie. Sie schreiben eine Funktion einmal und behalten trotzdem den konkreten Typ des Aufrufers. Der Ersatz any[] wirft genau die Information weg, für die Sie TypeScript benutzen.
  • Utility Types sparen echte Wartungsarbeit. Omit, Pick, Partial, Readonly, Record leiten Typen voneinander ab, statt sie zu duplizieren. Kopierte Typen laufen früher oder später auseinander.
  • any schaltet die Prüfung ab, unknown erzwingt sie. Unser Test: derselbe fehlerhafte Zugriff erzeugt mit unknown den Fehler TS18046, mit any null Fehler.
  • Der wichtigste Befund dieses Artikels: Ein Programm, das eine API-Antwort als getippten Wert behandelt, kompiliert ohne einen einzigen Fehler und stirbt zur Laufzeit mit TypeError: Cannot read properties of undefined. Wir zeigen den Beweis in beide Richtungen.
  • as ist eine Behauptung, keine Prüfung. Zwei as-Casts, 0 Compilerfehler, 1 Absturz.
  • Gegenmittel sind Type Guards und Schema-Validierung an genau drei Stellen: Netzwerk, Datei/Datenbank, Benutzereingabe. Dazwischen dürfen Sie den Typen vertrauen.
  • noUncheckedIndexedAccess ist die unterschätzteste Option. Sie fand in unserem Test einen Fehler, den strict: true allein durchgelassen hat.
🧪

Alles direkt ausprobieren, ohne Installation Jedes Beispiel aus diesem Artikel läuft in unserem TypeScript Playground: Code links, Typfehler und kompiliertes JavaScript rechts. Komplett in Ihrem Browser.

Zwei ähnliche durchscheinende Blaupausen-Container nebeneinander im Vergleich, der linke erweiterbar mit zusätzlichen Schichten, der rechte als versiegelte Einheit, dazwischen eine Waage

Voraussetzung: Was Sie schon können sollten

Dieser Artikel setzt voraus, dass Sie TypeScript grundsätzlich eingerichtet bekommen und wissen, was string, number und ein einfaches Objekt-Literal sind. Wenn Sie ganz am Anfang stehen — welche Reihenfolge, wie lange es dauert, wie Sie ein Projekt aufsetzen — lesen Sie zuerst unseren ehrlichen Lernpfad zu TypeScript und kommen dann hierher zurück.

Hier geht es um die Ebene danach: Wie typisiert man tatsächlich? Nicht „was ist ein Typ”, sondern „welche Konstruktion nehme ich in welcher Situation, und was kostet mich die falsche Wahl”.

Interface oder Type — die Frage, die kleiner ist als ihr Ruf

Kaum eine Frage im TypeScript-Umfeld erzeugt so viele Diskussionen bei so wenig praktischem Unterschied. Die ehrliche Antwort zuerst: In den allermeisten Fällen können Sie beides nehmen, und niemand wird es je merken.

Beide beschreiben die Form eines Objekts:

interface User {
  id: number;
  name: string;
}

type Admin = {
  id: number;
  role: string;
};

Beide prüfen gleich streng. Wenn Sie ein Feld hinzufügen, das nicht deklariert ist, bekommen Sie in beiden Fällen dieselbe Meldung. Hier die echte Ausgabe von tsc für ein zusätzliches Feld extra:

a.ts(4,39): error TS2353: Object literal may only specify known properties,
and 'extra' does not exist in type 'User'.

Diese Meldung ist übrigens ein eigenes Konzept, das viele überrascht: TypeScript prüft Objektliterale strenger als Variablen. Weisen Sie dasselbe Objekt erst einer Variable zu und diese dann dem Ziel, verschwindet die Meldung. Das ist Absicht („excess property check” nur bei frischen Literalen) und der Grund, warum manche Fehler beim Umbauen von Code plötzlich auftauchen oder verschwinden.

Die drei echten Unterschiede

1. Declaration Merging — nur Interfaces. Ein Interface kann mehrfach deklariert werden; TypeScript verschmilzt die Deklarationen:

interface Window {
  meineErweiterung: string;
}

Das ist der Grund, warum Sie fremde Typdefinitionen (etwa vom Browser oder einer Bibliothek) erweitern können, ohne deren Quellcode anzufassen. Bei type ist derselbe Name ein harter Fehler: Duplicate identifier.

Das ist gleichzeitig das Argument gegen Interfaces für interne Datenformen: Wenn jeder überall Felder anflanschen kann, wissen Sie beim Lesen einer Deklaration nicht mehr, ob sie vollständig ist.

2. Unions und alles, was kein Objekt ist — nur type. Das hier geht mit einem Interface schlicht nicht:

type Status = "offen" | "bezahlt" | "storniert";
type ID = string | number;
type Callback = (fehler: Error | null, daten?: string) => void;

Union Types sind in der Praxis das meistgenutzte Werkzeug überhaupt. Allein deshalb kommen Sie ohne type nicht aus.

3. Fehlermeldungen. Interfaces erscheinen in Meldungen unter ihrem Namen, komplexe type-Ausdrücke werden gelegentlich ausgeschrieben. Bei tief verschachtelten Typen macht das den Unterschied zwischen einer lesbaren und einer bildschirmfüllenden Fehlermeldung.

Die Regel, die wir tatsächlich anwenden

  • Objektformen, die andere erweitern können sollen (öffentliche API, Bibliotheks-Typen) → interface
  • Alles andere — Unions, Funktionssignaturen, abgeleitete Typen, interne Datenformen → type
  • Nicht in einem Team darüber streiten. Legen Sie eine Konvention fest, schreiben Sie sie in die Doku, und diskutieren Sie über etwas, das Nutzer merken.

Generics: einmal schreiben, Typ behalten

Generics sind der Punkt, an dem die meisten aussteigen — meist wegen der Notation, nicht wegen des Konzepts. Das Konzept ist simpel: ein Platzhalter für einen Typ, der erst beim Aufruf feststeht.

Der Nutzen wird am Gegenbeispiel klar. So sieht die Variante ohne Generics aus:

function firstBad(arr: any[]) { return arr[0]; }
function first<T>(arr: T[]): T | undefined { return arr[0]; }

const a = firstBad(["x"]);   // Typ: any
const b = first(["x"]);      // Typ: string | undefined

firstBad funktioniert — und wirft dabei jede Information weg. Ab hier weiß der Compiler nichts mehr über a, und jeder Tippfehler darauf bleibt unbemerkt. Bei first bleibt der Typ erhalten. Wenn wir das Ergebnis fälschlich als number behandeln, sagt der Compiler das im Klartext:

g1.ts(6,7): error TS2322: Type 'string | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.

Diese Meldung leistet zwei Dinge auf einmal: Sie meldet den falschen Grundtyp und erinnert daran, dass das Array leer sein könnte. Genau dafür ist das Typsystem da.

Eine wiederverwendbare Schablone, durch die verschiedenfarbige Objekte laufen und auf der anderen Seite ihre eigene Farbe und Identität behalten

Constraints: Platzhalter mit Mindestanforderung

Ein nackter Platzhalter T kann alles sein — also dürfen Sie auch fast nichts damit tun. Mit extends legen Sie eine Mindestanforderung fest:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("abc", "de");   // ok, Strings haben length
longest([1, 2], [3]);   // ok, Arrays haben length
longest(10, 20);        // Zahlen haben kein length

Die letzte Zeile ist der interessante Teil. Echte Ausgabe:

g2.ts(6,9): error TS2345: Argument of type 'number' is not assignable to
parameter of type '{ length: number; }'.

Beachten Sie, was hier nicht passiert: Die Funktion gibt weiterhin den konkreten Typ zurück, nicht { length: number }. longest("a","b") ist ein string, kein anonymes Objekt. Das ist der ganze Zweck der Übung — Anforderung stellen, ohne Information zu verlieren.

Wann Generics übertrieben sind

Generics haben Kosten: Lesbarkeit und Fehlermeldungen. Die Faustregel, die sich bei uns bewährt hat:

Ein Typparameter, der nur einmal vorkommt, ist meistens falsch. Der Sinn eines Platzhalters ist, zwei Stellen miteinander zu verknüpfen — Eingabe mit Ausgabe, oder zwei Parameter untereinander. Taucht T nur an einer einzigen Stelle auf, hätten Sie dort auch direkt den Typ hinschreiben können. Dann ist das Generic reine Dekoration.

Utility Types: nicht kopieren, ableiten

Utility Types sind eingebaute Typ-Funktionen, die aus bestehenden Typen neue machen. Ihr eigentlicher Wert ist nicht Tipparbeit — es ist die Kopplung. Ein abgeleiteter Typ ändert sich automatisch mit, ein kopierter Typ läuft irgendwann auseinander, und niemand merkt es.

Das folgende Beispiel ist aus der Praxis, weil es einen Sicherheitsaspekt hat:

interface User {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
}

type PublicUser = Omit<User, "passwordHash">;
type UserPatch  = Partial<Pick<User, "name" | "email">>;
type Ids        = Readonly<Record<"a" | "b", number>>;

Und hier die echten Fehlermeldungen, wenn man gegen diese Typen verstößt:

u.ts(5,61): error TS2353: Object literal may only specify known properties,
and 'passwordHash' does not exist in type 'PublicUser'.
u.ts(7,5): error TS2540: Cannot assign to 'a' because it is a read-only property.
u.ts(9,26): error TS2353: Object literal may only specify known properties,
and 'id' does not exist in type 'Partial<Pick<User, "email" | "name">>'.

Die erste Meldung ist die wichtige: Der Compiler verhindert, dass ein Passwort-Hash in eine Antwort rutscht, die nach außen geht. Kein Test, kein Review — eine Typdefinition. Und wenn jemand User später um ein Feld resetToken erweitert, ist das in PublicUser automatisch enthalten und muss bewusst ausgeschlossen werden. Bei einem handkopierten PublicUser wäre es stillschweigend nicht enthalten gewesen, was harmloser klingt, als es ist: Die Kopie hätte nie wieder eine Änderung mitbekommen.

Eine Hauptblaupause wird von leuchtenden Werkzeugen in vier kleinere abgeleitete Blaupausen verwandelt, eines entfernt Teile, eines macht Teile optional, eines versieht Teile mit Vorhängeschlössern

Die sieben, die im Alltag reichen

Utility TypeWas er tutTypischer Einsatz
Partial<T>Alle Felder optionalUpdate-/Patch-Objekte
Required<T>Alle Felder verpflichtendNach dem Auffüllen von Defaults
Pick<T, K>Nur die genannten FelderSchmale Sichten auf große Typen
Omit<T, K>Alle außer den genanntenSensible Felder entfernen
Readonly<T>Alle Felder schreibgeschütztKonfiguration, Konstanten
Record<K, V>Objekt mit festen SchlüsselnNachschlagetabellen
ReturnType<F>Rückgabetyp einer FunktionTypen aus Code ableiten

Alles darüber hinaus — Awaited, Parameters, NonNullable, Conditional Types — brauchen Sie erst, wenn Sie es brauchen. Wer sie vorsorglich lernt, vergisst sie wieder.

satisfies: prüfen, ohne den Typ zu verlieren

Das ist die nützlichste Neuerung der letzten Jahre und immer noch zu wenig bekannt. Das Problem: Eine Typ-Annotation prüft — und verbreitert dabei den Typ.

type Cfg = Record<string, string | number>;

const withAnnotation: Cfg = { port: 3000, host: "localhost" };
const withSatisfies = { port: 3000, host: "localhost" } satisfies Cfg;

withAnnotation.port.toFixed(0);   // Fehler
withSatisfies.port.toFixed(0);    // funktioniert

Die Ausgabe des Compilers beweist es — es gibt genau einen Fehler, und zwar in der annotierten Zeile:

x/s.ts(4,32): error TS2339: Property 'toFixed' does not exist on type 'string | number'.
  Property 'toFixed' does not exist on type 'string'.

Mit : Cfg weiß der Compiler nur noch „irgendwas aus string | number” und verweigert toFixed. Mit satisfies prüft er dieselbe Bedingung, merkt sich aber, dass port konkret die Zahl 3000 ist. Prüfung ohne Informationsverlust. Für Konfigurationsobjekte, Routentabellen und Konstanten-Maps ist das fast immer die richtige Wahl.

any gegen unknown: der Unterschied, der zählt

any ist keine Typangabe, sondern deren Abwesenheit. Es schaltet die Prüfung für diesen Wert komplett ab — und zwar ansteckend, weil alles, was daraus abgeleitet wird, ebenfalls any wird.

Wir haben beide Varianten mit demselben Fehler kompiliert. Zuerst unknown:

function parse(raw: string): unknown { return JSON.parse(raw); }
const data = parse('{"n":1}');
console.log(data.n);
b.ts(4,13): error TS18046: 'data' is of type 'unknown'.

Und jetzt dieselbe Sorglosigkeit mit any:

const val: any = "hello";
val.definitelyNotThere().alsoNotThere;
const n: number = val;

Ergebnis: keine einzige Fehlermeldung. Ein Aufruf einer Methode, die nicht existiert, auf dem Ergebnis ein weiterer Zugriff, und zum Schluss die Zuweisung eines Strings an eine Variable vom Typ number. Der Compiler schweigt zu allen dreien.

Das ist der Kern: unknown sagt „ich weiß es nicht, also beweise es mir”. any sagt „frag nicht nach”. Beide beschreiben dieselbe Unsicherheit, aber nur eines von beiden zwingt Sie, sie aufzulösen.

Zwei Tore nebeneinander, das linke weit offen mit ungeprüft durchströmenden Objekten, das rechte mit einer Scannerkontrolle, die jedes Objekt vor dem Durchlass prüft

Wann any trotzdem in Ordnung ist

Dogmatismus hilft hier nicht. Es gibt legitime Fälle:

  • Migration bestehenden Codes. Ein any mit einem // TODO-Kommentar ist besser als eine Woche Blockade. Wichtig ist, dass es auffindbar bleibt — @typescript-eslint/no-explicit-any als Warnung, nicht als Fehler.
  • Wirklich dynamische Grenzen, etwa ein generischer Plugin-Loader. Auch dort ist unknown plus Type Guard fast immer besser, nur eben teurer.

Was nicht in Ordnung ist: any als Standardlösung, wenn eine Fehlermeldung nervt. Das ist der Moment, in dem TypeScript aufhört, ein Werkzeug zu sein, und zu Dekoration wird.

Der wichtigste Abschnitt: Wo Ihre Typen aufhören zu schützen

Jetzt kommt der Teil, der in Tutorials fast immer fehlt — und der in unserer Erfahrung mehr Produktionsfehler verursacht als alle Generics-Feinheiten zusammen.

TypeScript existiert zur Laufzeit nicht. Die Typen werden vor der Ausführung entfernt. Was bleibt, ist JavaScript ohne jede Prüfung. Solange Ihre Daten innerhalb Ihres Codes bleiben, ist das kein Problem — der Compiler hat sie ja geprüft. Sobald Daten von außen kommen, ist jede Typangabe nur noch eine Behauptung.

Dieses Programm kompiliert fehlerfrei:

interface ApiUser { id: number; name: string }

function fromApi(payload: string): ApiUser {
  return JSON.parse(payload);
}

const u = fromApi('{"id":1}');   // 'name' fehlt in Wirklichkeit
console.log("Typprüfung sagt: alles in Ordnung");
console.log(u.name.toUpperCase());

tsc meldet null Fehler. JSON.parse gibt laut Typdefinition any zurück, und any passt auf alles — auch auf ApiUser. Der Compiler hat keinen Grund, sich zu beschweren.

Dann führt man es aus:

Typprüfung sagt: alles in Ordnung
/tmp/tstyping/proof.ts:5
console.log(u.name.toUpperCase());
                   ^
TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Das ist keine Theorie und kein konstruiertes Beispiel — das ist die häufigste Art, wie typisierte Projekte abstürzen. Der Compiler war grün, die Tests waren grün, und die API hat ein Feld weggelassen.

Dasselbe gilt für as. Wir haben zwei Casts geschrieben, einen davon absichtlich absurd:

const asUser = raw as User;                  // raw ist { id: 1 }
const bad = "5" as unknown as number;

0 Compilerfehler. Zur Laufzeit: TypeError: Cannot read properties of undefined (reading 'length'). Ein as behauptet nur — es prüft nichts. Der doppelte Cast über unknown ist die Notausstiegsluke, mit der Sie dem Compiler jeden Unsinn erzählen können.

Eine leuchtende senkrechte Grenzlinie zwischen einer geordneten Blaupausen-Welt links und einem chaotischen Strom ungeprüfter Rohdaten rechts, an der Grenze eine Filtermembran, die fehlerhafte Elemente auffängt

Die drei Grenzen, an denen Sie prüfen müssen

Sie müssen nicht überall validieren — das wäre teuer und unnötig. Es gibt genau drei Stellen, an denen Daten Ihr Typsystem betreten:

  1. Netzwerk — jede fetch-Antwort, jeder Webhook, jede Nachricht aus einer Queue
  2. Persistenz — Datenbank, Dateien, localStorage, Umgebungsvariablen
  3. Benutzereingabe — Formulare, URL-Parameter, hochgeladene Dateien

Innerhalb dieser Grenzen dürfen Sie Ihren Typen vertrauen. An den Grenzen ist ein Typ ohne Prüfung nur ein frommer Wunsch.

Lösung 1: Type Guards

Ein Type Guard ist eine gewöhnliche Funktion, deren Rückgabetyp v is User lautet. Läuft sie durch, weiß der Compiler ab dann Bescheid:

function isUser(v: unknown): v is User {
  return typeof v === "object" && v !== null
    && typeof (v as Record<string, unknown>).id === "number"
    && typeof (v as Record<string, unknown>).name === "string";
}

const raw: unknown = { id: 1 };
if (isUser(raw)) {
  console.log(raw.name.length);      // hier ist raw ein User
} else {
  console.log("abgewiesen: kein gueltiger User");
}
console.log(raw.name);               // außerhalb: wieder unknown

Zur Laufzeit gibt das Programm abgewiesen: kein gueltiger User aus — statt abzustürzen. Und die letzte Zeile, die außerhalb der Prüfung zugreift, meldet der Compiler:

guard.ts(10,13): error TS18046: 'raw' is of type 'unknown'.

Genau so soll es sein: Innerhalb des geprüften Zweigs voller Zugriff, außerhalb kein Vertrauensvorschuss.

Der Nachteil ist offensichtlich: Für jedes Feld eine Zeile, von Hand gepflegt. Bei einem verschachtelten Objekt mit zwanzig Feldern schreibt das niemand — und was niemand schreibt, schützt niemanden.

Lösung 2: Schema-Validierung (der Weg, den wir nehmen)

Bei mehr als ein paar Feldern ist ein Schema-Validator die bessere Antwort. Wir haben es mit Zod 4.4.3 gegengeprüft:

import { z } from "zod";

const ApiUser = z.object({ id: z.number(), name: z.string() });
type ApiUser = z.infer<typeof ApiUser>;

function fromApi(payload: string): ApiUser {
  return ApiUser.parse(JSON.parse(payload));
}

Derselbe fehlerhafte Payload wie oben ({"id":1}) liefert jetzt nicht mehr einen Absturz drei Funktionen später, sondern das hier — echte Ausgabe:

[
  {
    "expected": "string",
    "code": "invalid_type",
    "path": ["name"],
    "message": "Invalid input: expected string, received undefined"
  }
]

Der entscheidende Fortschritt ist nicht nur, dass der Fehler auffällt, sondern wo: an der Systemgrenze, mit dem Pfad zum betroffenen Feld. Vergleichen Sie das mit Cannot read properties of undefined irgendwo tief in der Aufrufkette.

Der zweite Gewinn ist z.infer: Schema und TypeScript-Typ kommen aus einer Quelle. Sie können nicht auseinanderlaufen. Das war bei handgeschriebenen Type Guards die eigentliche Gefahr — dass der Guard nach einer Feldänderung eine Wahrheit prüft, die es nicht mehr gibt.

Dasselbe Prinzip gilt übrigens für Sicherheit generell: Wir haben in unserem Artikel über IT-Sicherheitslücken an echten Vorfällen gezeigt, dass ungeprüfte Eingaben das häufigste Einfallstor überhaupt sind. Typen ersetzen keine Validierung — sie beschreiben nur, was nach der Validierung gelten soll.

Mapped Types: Typen aus Typen erzeugen

Ein Schritt weiter als Utility Types: Sie können selbst welche bauen. Mapped Types laufen über die Schlüssel eines Typs und erzeugen daraus neue.

type Events = {
  click: { x: number; y: number };
  keypress: { key: string };
};

type Handlers = {
  [K in keyof Events as `on${Capitalize<K>}`]: (e: Events[K]) => void
};

Daraus entsteht automatisch ein Typ mit den Feldern onClick und onKeypress — jeweils mit dem passenden Ereignisobjekt. Wenn Sie im onClick-Handler auf ein Feld des Tastatur-Ereignisses zugreifen, sagt der Compiler:

mapped.ts(7,57): error TS2339: Property 'key' does not exist on type '{ x: number; y: number; }'.

Das ist der Punkt, an dem sich Typisierung wirklich auszahlt: Ein neues Ereignis in Events erzeugt automatisch den passenden Handler-Typ. Keine zweite Liste, die jemand vergessen kann.

Warnung aus der Praxis: Mapped Types und Conditional Types sind der Bereich, in dem TypeScript-Code unlesbar wird. Unsere Regel: Ein selbstgebauter Typ, den ein Kollege nicht in dreißig Sekunden versteht, braucht entweder einen Kommentar oder sollte zwei einfache Typen sein. Cleverness im Typsystem wird beim Debuggen mit Zinsen zurückgezahlt.

Discriminated Unions: das unterschätzte Arbeitspferd

Wenn wir nur ein einziges Muster empfehlen dürften, wäre es dieses. Ein gemeinsames Feld mit festem Wert macht aus einer Union einen präzise unterscheidbaren Typ:

type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; s: number };

function area(sh: Shape) {
  if (sh.kind === "circle") return Math.PI * sh.r ** 2;
  return sh.s * sh.s;
}

Der Compiler versteht die Abfrage auf kind und weiß in jedem Zweig, welche Felder existieren. Greifen Sie ohne Prüfung zu, gibt es sofort eine Meldung — inklusive Hinweis, welches Mitglied der Union das Feld nicht hat:

n.ts(9,41): error TS2339: Property 'r' does not exist on type 'Shape'.
  Property 'r' does not exist on type '{ kind: "square"; s: number; }'.

Dasselbe Muster löst das ewige Problem mit Ergebnistypen sauber:

type Result<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

Nach if (res.ok) gibt es data und kein error, danach umgekehrt. Kein data?: T, bei dem Sie überall ! oder Fragezeichen brauchen. Machen Sie unmögliche Zustände unmöglich — statt sie durch optionale Felder erst zu erlauben und dann überall zu prüfen.

Die Compiler-Optionen, die tatsächlich etwas ändern

strict: true ist der Sammelschalter und sollte in jedem neuen Projekt an sein. Was er konkret bewirkt, sehen Sie an dieser echten Meldung:

n.ts(2,18): error TS18047: 'name' is possibly 'null'.

Ohne strictNullChecks (Teil von strict) wäre das durchgegangen — und später als Cannot read properties of null in Erscheinung getreten. Diese eine Option verhindert die mit Abstand häufigste Fehlerklasse in JavaScript.

noUncheckedIndexedAccess — die Option, die strict nicht enthält

Diese ist die interessanteste, weil fast niemand sie kennt. Wir haben es gemessen:

const names: string[] = ["a", "b"];
const n = names[99];
console.log(n.toUpperCase());

Mit strict: true allein: kein Fehler. TypeScript nimmt an, dass ein Zugriff auf string[] einen string liefert — auch bei Index 99. Mit der Option:

idx.ts(3,13): error TS18048: 'n' is possibly 'undefined'.

Das ist die Wahrheit über JavaScript-Arrays, und strict sagt sie standardmäßig nicht. Der Preis ist real: In Schleifen und bei Lookup-Tabellen kommen viele neue undefined-Prüfungen dazu. Unsere Empfehlung: in neuen Projekten von Anfang an an, in bestehenden nur mit Zeitbudget einschalten.

OptionIn strict enthalten?Was sie fängt
strictNullChecks✅ janull/undefined-Zugriffe
noImplicitAny✅ jaVergessene Parametertypen
strictFunctionTypes✅ jaUnsichere Callback-Signaturen
noUncheckedIndexedAccessneinArray-/Objektzugriffe ins Leere
exactOptionalPropertyTypes❌ neinundefined explizit gesetzt vs. fehlend
noImplicitOverride❌ neinVersehentlich überschriebene Methoden

Häufige Fragen

Was bedeutet „Typing” bei TypeScript genau? Typing bezeichnet das Beschreiben der Datenformen in Ihrem Programm — welche Felder ein Objekt hat, was eine Funktion annimmt und zurückgibt, welche Werte erlaubt sind. TypeScript prüft diese Beschreibungen vor der Ausführung und entfernt sie danach. Zur Laufzeit ist keine einzige Typangabe mehr vorhanden.

Interface oder Type — was soll ich nehmen? Beides funktioniert für Objektformen gleich gut. Nehmen Sie interface, wenn andere den Typ erweitern können sollen (Bibliotheks-APIs, Erweiterung fremder Typen). Nehmen Sie type für alles andere, insbesondere für Unions, Funktionssignaturen und abgeleitete Typen — denn Unions gehen mit Interfaces gar nicht.

Warum ist any schlecht, wenn es doch funktioniert? Weil es die Prüfung nicht nur für diesen Wert abschaltet, sondern auch für alles, was daraus entsteht. In unserem Test erzeugte ein Aufruf einer nicht existierenden Methode plus eine falsche Zuweisung an eine number-Variable null Fehlermeldungen. Derselbe Code mit unknown meldet sofort TS18046. unknown erzwingt die Klärung, any verhindert sie.

Schützen mich TypeScript-Typen vor fehlerhaften API-Antworten? Nein, und das ist die wichtigste Erkenntnis dieses Artikels. Wir haben ein Programm kompiliert, das eine unvollständige API-Antwort als getippten Wert behandelt: null Compilerfehler, dann zur Laufzeit TypeError: Cannot read properties of undefined. An Systemgrenzen brauchen Sie echte Prüfung — Type Guards oder ein Schema-Validator wie Zod.

Wann brauche ich Generics wirklich? Immer dann, wenn zwei Stellen typmäßig verknüpft bleiben sollen: Eingabe mit Ausgabe, oder zwei Parameter untereinander. Kommt Ihr Typparameter nur an einer einzigen Stelle vor, brauchen Sie ihn nicht — dann können Sie den konkreten Typ direkt hinschreiben.

Was ist der Unterschied zwischen satisfies und einer Typ-Annotation? Eine Annotation (const x: Cfg = …) prüft und verbreitert den Typ danach auf Cfg. satisfies prüft dieselbe Bedingung, behält aber den konkreten Typ. In unserem Test scheiterte withAnnotation.port.toFixed(0) mit TS2339, während die identische Zeile mit satisfies funktionierte.

Sollte ich noUncheckedIndexedAccess einschalten? In neuen Projekten ja, von Anfang an. Sie fing in unserem Test einen Zugriff auf Index 99 eines zweielementigen Arrays ab, den strict: true allein durchließ. In bestehenden Projekten erzeugt sie viele neue Meldungen auf einmal — dort nur mit eingeplanter Zeit einschalten.

Wie viele Typen sind zu viele? Wenn Sie mehr Zeit mit dem Typsystem als mit dem Problem verbringen, sind Sie zu weit. Konkreter Test: Wenn ein Kollege einen Ihrer Typen nicht in dreißig Sekunden versteht, braucht er einen Kommentar — oder sollte in zwei einfache Typen zerfallen.

Werkzeuge, die dabei helfen

Der Playground zum Ausprobieren. Bei Fragen der Art „was passiert, wenn ich…” ist ein Projekt aufzusetzen zu langsam. Unser TypeScript Playground läuft direkt im Browser: Code links, Typfehler und kompiliertes Ergebnis rechts, keine Installation.

Der Editor als Hauptwerkzeug. Der größte Teil des Nutzens von Typisierung entsteht nicht beim Kompilieren, sondern beim Schreiben: Autovervollständigung, „Gehe zu Definition”, sichere Umbenennung über Dateigrenzen hinweg. Wenn Ihre Typen gut sind, wird der Editor zu einer Dokumentation, die nie veraltet.

KI-Assistenten mit Vorsicht. Sie sind gut darin, ein Zod-Schema aus einer Beispielantwort zu erzeugen oder einen Mapped Type zu erklären. Sie sind schlecht darin, aktuelle Versionsstände zu kennen — siehe unsere Übersicht der besten KI-Coding-Tools. Prüfen Sie Vorschläge zu neueren Sprachfeatures immer gegen die echte Compilerausgabe.

Framework-Kontext. Wie Typisierung in einem konkreten Framework aussieht — etwa bei dynamischen Routen und deren Parametern — zeigen wir am Beispiel in Next.js Dynamic Routes.

Fazit: Typen sind ein Werkzeug, kein Selbstzweck

Gutes TypeScript Typing erkennt man nicht an der Cleverness der Typen, sondern daran, dass niemand mehr über sie redet. Die Konstruktionen, die den größten Nutzen bringen, sind unspektakulär: Union Types mit Unterscheidungsfeld, ein paar Utility Types statt kopierter Deklarationen, unknown statt any, und eine echte Prüfung an den drei Systemgrenzen.

Die Sache, die wir jedem mitgeben würden, ist die unbequemste: Ein grüner Compiler beweist nicht, dass Ihre Daten die behauptete Form haben. Er beweist, dass Ihr Code in sich schlüssig ist. Zwischen diesen beiden Aussagen liegt jeder TypeError, den wir in diesem Artikel echt erzeugt haben — und das Programm, das dabei stolz Typprüfung sagt: alles in Ordnung ausgab, bevor es abstürzte.

Wenn Sie nach dem Lesen genau zwei Dinge ändern: Ersetzen Sie any durch unknown und lösen Sie die daraus entstehenden Fehler auf. Und schreiben Sie ein Schema für die eine API-Antwort, der Ihr Code am blindesten vertraut. Diese beiden Handgriffe fangen mehr Produktionsfehler ab als jeder Conditional Type, den Sie je schreiben werden.