Zum Danebenlegen, während man modelliert: Welcher Baustein sagt was, welche Prüfung verengt worauf, wo hört der Compiler auf zu helfen. Die zweite Hälfte des Seminars — Toolchain, Migration, Coding Agents — steht auf dem Blatt TypeScript 7 im Projekt.
Maßgeblich bleibt die TypeScript-Dokumentation; dieses Blatt ist eine Auswahl, keine Referenz. Wie man die Entscheidungen trifft, ist die Seminarleistung — hier steht nur, wie sie heißen.
Compile-Time und Laufzeit
Ein Typ beschreibt eine Absicht und erzwingt zur Laufzeit nichts. Beim Übersetzen verschwindet er restlos (Type Erasure) — das ist die Aussage, an der alles Weitere hängt.
| Ebene | Legt fest | Wird durchgesetzt von |
|---|---|---|
| ECMAScript | Was die Sprache können muss | dem Standardisierungsgremium |
| JavaScript | Was zur Laufzeit passiert | der Engine im Browser oder in Node |
| TypeScript | Was der Code behaupten darf | dem Compiler, vor der Auslieferung |
Kein Typ überlebt die Netzwerkgrenze, den Dateizugriff oder das Formular. Wo Daten von außen kommen, ist der Typ eine Behauptung; geprüft wird separat (siehe „Die Grenze zu externen Daten”).
Modul: TypeScript 7 im JavaScript-Ökosystem
Grundtypen: welcher Baustein sagt was
| Baustein | Aussage | Wann falsch gewählt |
|---|---|---|
| Array | beliebig viele, alle gleich | wenn die Anzahl fachlich feststeht |
| Tupel | feste Anzahl, feste Bedeutung je Stelle | wenn die Liste wachsen darf |
| Objekttyp | bekannte Schlüssel | wenn die Schlüssel erst zur Laufzeit entstehen |
| Index-Signatur | beliebige Schlüssel, ein Werttyp | wenn die Schlüssel bekannt sind |
type Preise = number[]; // beliebig viele
type Zeitraum = [von: string, bis: string]; // genau zwei, benannt
Benannte Tupelstellen kosten nichts und ersparen die Rückfrage, welcher der beiden Werte der Beginn ist.
Modul: Typinferenz und sichere Grundtypen
any, unknown, never, void
| Typ | Bedeutet | Richtig eingesetzt bei |
|---|---|---|
| any | prüf hier nichts mehr | fast nie, allenfalls als Zwischenschritt |
| unknown | irgendetwas, erst prüfen | allen Daten von außerhalb des Programms |
| never | kann nicht eintreten | Exhaustiveness-Prüfungen |
| void | gibt nichts zurück | Rückrufen, deren Ergebnis egal ist |
unknown lässt sich nicht versehentlich benutzen, nur bewusst prüfen — der Weg
zum Domänentyp führt zwangsläufig durch eine Prüfstelle, und die ist im Diff
sichtbar. any breitet sich dagegen aus: Was es berührt, wird ebenfalls
ungeprüft.
Modul: Typinferenz und sichere Grundtypen
Was strict wirklich einschließt
| Option | in strict? | Was sie zusätzlich prüft |
|---|---|---|
| strictNullChecks | ja | null und undefined als eigene Fälle |
| noImplicitAny | ja | keine stillen any-Parameter |
| exactOptionalPropertyTypes | nein | fehlend ist nicht dasselbe wie undefined |
| noUncheckedIndexedAccess | nein | Indexzugriffe können ins Leere greifen |
Seit TypeScript 6.0 ist strict voreingestellt — die beiden unteren Optionen
bleiben trotzdem einzeln zu setzen. „Wir sind strict” ist deshalb keine Angabe,
solange nicht dazugesagt wird, welche Strenge gemeint ist.
Drei Schreibweisen, drei verschiedene Aussagen:
interface Auftrag {
readonly nummer: string; // da, nie änderbar
abholnotiz?: string; // darf ganz fehlen
betragCent: number | undefined; // da, evtl. leer
}
readonly schützt die Eigenschaft, nicht das Objekt — und gilt beim
Übersetzen, nicht zur Laufzeit.
Modul: Typinferenz und sichere Grundtypen
Structural Typing, Interface oder Type Alias
| Frage | Interface | Type Alias |
|---|---|---|
| Objektformen beschreiben | ja | ja |
| Unions, Tupel, Bedingtes | nein | ja |
| Nachträglich erweiterbar | ja, offen | nein, geschlossen |
| Empfehlung im Seminar | öffentliche Objektformen | alles Übrige |
Ein Type Alias auf string erzeugt keinen eigenen Typ, sondern nur einen
zweiten Namen: Auftragsnummer und Kundennummer bleiben austauschbar, bis
eine Marke dazukommt (siehe „Branded Types”).
Modul: Typinferenz und sichere Grundtypen
as const und satisfies
const orte = ["Talbrück", "Hafenrand"] as const;
type Filiale = (typeof orte)[number]; // Union aus der Liste
const werkstatt = {
Talbrück: { plaetze: 6 },
Hafenrand: { plaetze: 3 },
} satisfies Record<Filiale, { plaetze: number }>;
werkstatt.Talbrück.plaetze; // eng, kein Record
as const macht eine Liste zur Typquelle — Wert und Typ können nicht mehr
auseinanderlaufen. satisfies prüft gegen einen Zieltyp, ohne die genauen
Schlüssel zu verlieren; eine Annotation an derselben Stelle täte das. Beides
wirkt beim Übersetzen, nicht zur Laufzeit. Und: Annotation und satisfies
gleichzeitig gesetzt — die Annotation gewinnt.
Modul: Typinferenz und sichere Grundtypen
Narrowing: welche Prüfung verengt worauf
| Prüfung | Verengt auf | Grenze |
|---|---|---|
| typeof | primitive Typen | kennt keine Objektformen |
| instanceof | Klassen und ihre Instanzen | scheitert über Realm-Grenzen |
| in | Objekte mit dieser Eigenschaft | prüft Vorhandensein, nicht den Typ |
| Vergleich auf Literal | genau diesen Fall | braucht ein Unterscheidungsfeld |
Ein eigenes Typprädikat (a is Freigegeben) lohnt, wenn die Erkennung mehrere
Bedingungen braucht, an vielen Stellen gebraucht wird oder einen fachlichen
Namen verdient. Seit TypeScript 5.5 leitet der Compiler Prädikate selbst ab —
aber nur, wenn die Funktion direkt einen Vergleich zurückgibt.
Modul: Union Types und Control Flow Analysis
Discriminated Unions und Vollständigkeit
type Auftrag =
| { zustand: "angenommen"; nummer: Auftragsnummer }
| { zustand: "voranschlagOffen"; cent: number }
| { zustand: "freigegeben"; cent: number }
| { zustand: "abgeholt"; belegNr: string };
function text(a: Auftrag): string {
switch (a.zustand) {
case "angenommen": return "in Annahme";
// …
default: return nieErreicht(a);
}
}
function nieErreicht(x: never): never { throw x; }
Der Gewinn: Ein angenommener Auftrag hat keinen Betrag — nicht undefined,
sondern gar nicht. Kein Feld muss optional sein, nur weil es nicht überall
vorkommt. Die never-Zeile macht daraus einen Vollständigkeitsbeweis: Ein
neuer Zustand bricht beim Übersetzen, nicht im Betrieb.
Schalter gegen Zustand — der Umbau, der sich am häufigsten lohnt:
| Frage | Boolesche Schalter | Zustandstyp |
|---|---|---|
| Darstellbare Kombinationen | acht bei drei Feldern | genau die gültigen |
| Unmöglicher Zustand | jederzeit erzeugbar | nicht hinschreibbar |
| Neuer Fall | ein weiterer Schalter, 16 Fälle | ein weiteres Glied |
| Lesbarkeit im Review | Kombinationen im Kopf | am Namen ablesbar |
Erkennungszeichen im Bestand: zwei Schalter, die nie gleichzeitig wahr sein dürfen, und Kommentare der Sorte „nur gesetzt wenn”.
Modul: Union Types und Control Flow Analysis
Ausnahme oder Ergebniswert
| Situation | Ausnahme | Ergebniswert |
|---|---|---|
| Fachlich erwartbarer Fall | nein | ja |
| Programmierfehler, Zustandsbruch | ja | nein |
| Fehler soll durchgereicht werden | ja | umständlich |
| Aufrufer muss reagieren | leicht vergessen | erzwungen |
type Ergebnis<T> =
| { ok: true; wert: T }
| { ok: false; fehler: string };
Modul: Union Types und Control Flow Analysis
Generics: keyof, typeof, Indexed Access
| Operator | Nimmt | Liefert |
|---|---|---|
| keyof | einen Typ | die Union seiner Schlüssel |
| typeof | einen Wert | dessen Typ |
| T[“feld”] | Typ und Schlüssel | den Typ dieser Eigenschaft |
| T[keyof T] | einen Typ | die Union aller Werttypen |
Ein Typparameter, der nur an einer Stelle vorkommt, ist in Wahrheit any. Ein
Constraint nennt genau das, was der Rumpf braucht — nicht mehr (schließt gültige
Aufrufer aus) und nicht weniger (zwingt zu Prüfungen im Rumpf).
function nummer<T extends { nummer: string }>(x: T) { return x.nummer; }
type Seite<T = Auftrag> = { treffer: T[] };
type Wert<T> = T extends Ergebnis<infer W> ? W : never;
type Entwurf<T> = { [K in keyof T]?: T[K] };
type Ereignis = `auftrag:${"offen" | "fertig"}`;
Modul: Generics und wiederverwendbare APIs
Utility Types
| Utility Type | Macht | Typischer Einsatz |
|---|---|---|
| Partial | alle Eigenschaften optional | Entwürfe und Teilaktualisierungen |
| Pick, Omit | Auswahl aus einem Typ | Sichten für eine Schnittstelle |
| Record | Schlüssel auf Werttyp | Konfigurationen |
| Readonly | alles schreibgeschützt | Werte, die nicht mutiert werden |
| ReturnType | Rückgabe einer Funktion | Ableiten statt Wiederholen |
Ein eigener Utility Type lohnt ab der dritten Verwendung — und dann nur mit einem Namen, der seine Absicht verrät. Dass er kürzer aussieht als die Standardvariante, ist kein Grund.
Modul: Generics und wiederverwendbare APIs
Branded Types
declare const marke: unique symbol;
type Marke<T, N> = T & { readonly [marke]: N };
type Auftragsnummer = Marke<string, "Auftrag">;
type Kundennummer = Marke<string, "Kunde">;
const k = "K-8842" as Kundennummer;
const a: Auftragsnummer = k; // jetzt ein Fehler
Lohnt bei Kennungen, die zur Laufzeit gleich aussehen und nie vertauscht werden dürfen; bei Werten mit Einheit (Cent gegen Euro, Sekunden gegen Millisekunden); und für geprüfte Daten — ein geprüfter Wert bekommt die Marke, ein ungeprüfter nicht. Die Marke existiert nur beim Übersetzen; zur Laufzeit bleibt es eine Zeichenkette.
Modul: Generics und wiederverwendbare APIs
Die Grenze zu externen Daten
| Quelle | Typisches Format | Was regelmäßig überrascht |
|---|---|---|
| API-Antwort | JSON | Feld fehlt, null statt Wert, Zahl als Text |
| Formular | Zeichenketten | alles ist Text, auch Zahlen und Datumsangaben |
| Konfiguration | Umgebung, Datei | leere Zeichenkette gilt als gesetzt |
| Datei-Import | JSON, CSV | Kodierung, Trennzeichen, fehlende Spalten |
// Vorsicht: json() liefert any, nicht unknown
const roh: unknown = await antwort.json();
const teil = zuTeil(roh); // prüft und wandelt um
Die Annotation auf unknown ist der ganze Trick — sie macht aus einem bequemen
any eine erzwungene Entscheidung. Ein Ort für die Prüfung, nicht fünf.
Modul: Runtime-Sicherheit und externe Daten
Von Hand oder mit Schema
| Kriterium | Von Hand | Schema-Bibliothek |
|---|---|---|
| Kleines Modell | transparent, angemessen | Overhead ohne Gewinn |
| Großes Modell | wächst schlecht, fehleranfällig | trägt |
| Typ und Prüfung | zwei Quellen, laufen auseinander | eine Quelle |
| Fehlermeldung | selbst zu bauen | mitgeliefert, mit Pfadangabe |
| Abhängigkeit | keine | eine, mit eigener Fehlerkultur |
Bei type-first muss jemand daran denken, Typ und Prüfung gemeinsam zu ändern; bei schema-first gibt es nur eine Seite, der Typ wird abgeleitet. Was Generierung aus OpenAPI oder JSON Schema dabei leistet — und was nicht:
| Erwartung | Trifft zu | Trifft nicht zu |
|---|---|---|
| Typ passt zur Beschreibung | ja | wenn die Beschreibung falsch ist |
| Typ passt zu den echten Daten | nein | Beschreibung ist kein Vertrag |
| Prüfung entfällt | nein | Type Erasure gilt unverändert |
| Änderung fällt auf | ja, im Diff | nur wenn neu generiert wird |
Für Antworten eines Sprachmodells gilt nichts anderes: Ein angefordertes Schema ist nicht garantiert eingehalten, und dass die Antwort im eigenen Prozess entstanden ist, ändert an Type Erasure nichts.
Modul: Runtime-Sicherheit und externe Daten
Typische Fallen
- Erwarten, dass eine Typannotation eine API-Antwort validiert. Sie behauptet nur; geprüft wird an der Grenze.
anyeinsetzen, um eine Fehlermeldung loszuwerden. Sie kommt später wieder, dann im Betrieb.unknownmit einer Assertion in den Zieltyp zwingen statt zu prüfen.json()ohne Annotation verwenden — das Ergebnis istany, nichtunknown.typeof nullistobject. Der klassische Fallstrick, der bis heute trägt.- Verengung nach
awaitoder in einer Zwischenvariablen verloren. Der Compiler verfolgt sie dort nicht mehr. - Das Unterscheidungsfeld als
stringtypisieren statt als Literal — dann verengt nichts mehr. - Sich auf die Reihenfolge der Union-Glieder verlassen, etwa in Snapshot-Tests. Sie war nie zugesichert und ist in TypeScript 7 anders.
readonlyfür Laufzeitschutz halten und auf Kopien verzichten.- Optional Chaining überall streuen und den Fehlerfall damit unsichtbar machen.
- Index-Signaturen einsetzen, obwohl die Schlüssel bekannt sind.
- Conditional Types verschachteln, bis die Fehlermeldung unlesbar wird — spätestens dann liest sie im Review niemand mehr.