JavaScript kennt zwei Werte für „nichts": null und undefined. Beide bezeichnen die Abwesenheit eines Wertes, meinen aber semantisch Unterschiedliches — und genau aus dieser Doppelung entsteht ein Großteil aller Laufzeitfehler, die TypeScript heute verhindern kann. Mit aktiviertem strictNullChecks werden beide Werte zu eigenständigen, sichtbaren Typen, die der Compiler an jeder Zugriffsstelle einfordert. Dieser Artikel zeigt den semantischen Unterschied, warum strictNullChecks für neue Projekte nicht verhandelbar ist, und welche Sprach-Werkzeuge — Optional Chaining ?., Nullish Coalescing ??, Type Guards und die Non-Null-Assertion ! — den Alltag mit nullable Typen schmerzfrei machen. Wer diese Muster verinnerlicht, schreibt TypeScript-Code, in dem Cannot read properties of undefined praktisch nicht mehr vorkommt.
null vs. undefined — semantische Trennung
JavaScript trägt eine historische Eigenheit: zwei Werte für Abwesenheit, mit feiner aber konsistenter Bedeutungsunterscheidung.
undefinedentsteht automatisch, wenn eine Variable deklariert aber nicht zugewiesen wurde, eine Funktion ohnereturnendet, ein Funktions-Parameter fehlt, oder eine Objekt-Property nicht existiert.nullist ein bewusster Wert — vom Entwickler explizit gesetzt, um „hier steht absichtlich nichts" auszudrücken. JavaScript erzeugtnullnie von selbst (mit der berühmten Ausnahmetypeof null === "object").
let nieZugewiesen: string | undefined;
console.log(nieZugewiesen); // undefined — automatisch
function nichts() { /* kein return */ }
console.log(nichts()); // undefined — Rückgabe-Default
const absichtlichLeer: string | null = null; // bewusst gesetzt
const obj: { name?: string } = {};
console.log(obj.name); // undefined — Property fehltKonvention in der Praxis: Moderne TypeScript-Codebasen verwenden überwiegend undefined für „kein Wert vorhanden" und reservieren null für Domänen-Modelle, in denen „explizit leer" eine andere Bedeutung hat als „nicht gesetzt" — etwa bei Formular-Feldern, die der Nutzer aktiv geleert hat, oder bei Datenbank-Spalten, die NULL erlauben. APIs wie Map.get(), Array.find(), document.querySelector() und JSON.parse() liefern undefined bzw. null je nach Konvention der Schnittstelle.
Ohne strictNullChecks — die klassische Falle
Vor strictNullChecks (und in Legacy-Projekten ohne strict: true) sind null und undefined unsichtbare Mitglieder jedes Typs. Der Typ string umfasst dann implizit auch null und undefined, und der Compiler beschwert sich nirgendwo.
// tsconfig: "strictNullChecks": false
const users = [
{ name: "Anna", age: 30 },
{ name: "Bea", age: 25 },
];
const found = users.find(u => u.name === "Carl");
console.log(found.age); // Compiler-OK — Laufzeit: TypeErrorArray.prototype.find() gibt T | undefined zurück. Ohne strictNullChecks ignoriert der Compiler das | undefined, und der Zugriff .age auf ein undefined-Ergebnis schlägt erst zur Laufzeit zu. Das ist die Nummer-eins-Quelle für TypeError: Cannot read properties of undefined in TypeScript-Codebasen und der Grund, warum der Compiler-Flag heute als Default in jeder modernen tsconfig.json steht.
strictNullChecks: true — der Pflicht-Schalter
Sobald strictNullChecks aktiv ist (was bei strict: true automatisch passiert, und das ist Default seit tsc --init), werden null und undefined zu eigenständigen Typen, die der Compiler überall sichtbar macht.
// tsconfig: "strict": true (impliziert strictNullChecks)
const found = users.find(u => u.name === "Carl");
console.log(found.age);
// ^^^^^ Error: 'found' is possibly 'undefined'.
// Erzwingt Behandlung:
if (found) {
console.log(found.age); // OK — gennarrowed auf { name, age }
}Was sich konkret ändert:
nullundundefinedsind nicht mehr zuweisungskompatibel zu beliebigen Typen.- Nullable-Typen müssen explizit als Union notiert werden:
string | null,string | undefined. - Vor jedem Zugriff auf einen möglicherweise leeren Wert verlangt der Compiler Narrowing — durch
if-Check, Optional Chaining oder Non-Null-Assertion. - Optionale Funktions-Parameter (
name?: string) sind automatischstring | undefined.
Für neue Projekte ist strict: true nicht verhandelbar. Die einzige legitime Ausnahme: inkrementelle Migration einer großen Legacy-Codebasis, in der man strictNullChecks separat und schrittweise aktiviert.
Union mit null und undefined
Mit strictNullChecks musst du Nullbarkeit explizit modellieren. Drei häufige Varianten:
| Typ | Bedeutung | Typischer Einsatz |
|---|---|---|
string | undefined | Wert fehlt oder ist noch nicht gesetzt | Optionale Parameter, fehlende Map-Einträge, find()-Ergebnisse |
string | null | Wert ist explizit als „leer" markiert | Domänen-Modelle, DB-Spalten mit NULL, geleerte Formularfelder |
string | null | undefined | Beide Zustände sind möglich | API-Antworten externer Schnittstellen, JSON-Schemas |
Praxis-Regel: Im internen Code möglichst auf einen der beiden Werte festlegen — meist undefined. Wer beide unkritisch durcheinander mischt, zwingt jeden Call-Site-Check zur Behandlung beider Fälle.
// Intern: nur undefined
function findUser(id: string): User | undefined { /* ... */ }
// API-Grenze: Backend liefert null — sofort normalisieren
type ApiUser = { name: string; email: string | null };
function fromApi(u: ApiUser): User {
return { ...u, email: u.email ?? undefined };
}Optional Properties vs. | undefined
Ein subtiler aber wichtiger Unterschied:
type A = { name?: string }; // Property kann FEHLEN
type B = { name: string | undefined }; // Property MUSS DA SEIN, darf undefined
const a1: A = {}; // OK
const a2: A = { name: undefined }; // OK (ohne exactOptionalPropertyTypes)
const b1: B = {}; // Error: Property 'name' fehlt
const b2: B = { name: undefined }; // OKexactOptionalPropertyTypes (seit TS 4.4): Ist dieses strikte Flag aktiv, akzeptiert A nur das Fehlen der Property, nicht den expliziten Wert undefined — name?: string wird dann zu echtem „optional", ohne den Trostpreis | undefined.
// mit exactOptionalPropertyTypes: true
const a3: A = { name: undefined };
// ^^^^ Error: Type 'undefined' is not assignable to type 'string'.Empfehlung: Bei neuen Projekten direkt mit exactOptionalPropertyTypes: true starten — der semantische Unterschied zwischen „nicht gesetzt" und „auf undefined gesetzt" wird dann sauber sichtbar.
Optional Chaining ?.
Seit TypeScript 3.7 erlaubt der ?.-Operator sicheren Property-, Element- und Methoden-Zugriff auf möglicherweise nullishe Werte. Ist der linke Ausdruck null oder undefined, kurzschließt die Kette und liefert undefined — ohne TypeError.
type User = {
name: string;
address?: { street: string; city?: string };
friends?: User[];
greet?: (loud: boolean) => string;
};
function describe(u: User | undefined) {
// Property-Zugriff
const city = u?.address?.city; // string | undefined
// Array-Element
const firstFriend = u?.friends?.[0]; // User | undefined
// Methoden-Aufruf
const hello = u?.greet?.(true); // string | undefined
return { city, firstFriend, hello };
}Wichtig: ?. prüft nur den direkten linken Operanden. u?.address.city schützt vor u === undefined, nicht vor address === undefined. Wer auf zwei Ebenen unsicher ist, braucht zwei ?..
?. ist auch zur Laufzeit eine Sprachfunktion — das transpilierte JavaScript prüft tatsächlich auf == null (was null und undefined abdeckt).
Nullish Coalescing ??
Der ??-Operator liefert einen Default-Wert nur, wenn die linke Seite null oder undefined ist — nicht bei anderen „falsy"-Werten wie 0, "" oder false. Genau das unterscheidet ihn vom altbekannten ||.
const volume1 = 0 || 0.5; // ! 0.5 — 0 ist falsy
const volume2 = 0 ?? 0.5; // + 0 — 0 ist nicht nullish
const name1 = "" || "Anon"; // ! "Anon"
const name2 = "" ?? "Anon"; // + ""
const flag1 = false || true; // ! true
const flag2 = false ?? true; // + false
console.log(volume1, volume2, name1, name2, flag1, flag2);0.5 0 Anon true falseFaustregel: Für Defaults nullable Werte immer ?? verwenden. || nur dort, wo man bewusst alle falsy-Werte ersetzen möchte (z. B. „leerer String soll als 'kein Name' gelten").
Kombination mit Optional Chaining ist idiomatisch:
const city = user?.address?.city ?? "Unbekannt";
const items = response?.data?.items ?? [];Non-Null-Assertion !
Der postfix !-Operator sagt dem Compiler: „Vertrau mir, hier ist nichts null oder undefined." Das ist eine reine Typ-Aussage — zur Laufzeit passiert nichts, kein Check, kein Throw.
function shout(value: string | undefined) {
console.log(value!.toUpperCase());
// ^ — Compiler glaubt: value ist string
}
shout(undefined); // Laufzeit: TypeError: Cannot read properties of undefinedLegitime Einsatzbereiche sind eng:
- DOM-Selektoren mit garantierter Existenz:
document.getElementById("root")! - Class-Properties, die nicht im Konstruktor, sondern in
init()gesetzt werden — besser via!an der Deklaration:private socket!: WebSocket; - Test-Setups, wo
beforeEachWerte garantiert vorbelegt.
Gefährlich wird ! immer dann, wenn das Wissen über Nicht-Null nicht im Compiler erkennbar ist und sich später ändern kann. Bessere Alternative ist fast immer ein expliziter Type Guard:
// Schlecht — lügt das Typsystem an
function shoutBad(value: string | undefined) {
return value!.toUpperCase();
}
// Gut — Compiler kann mitdenken
function shoutGood(value: string | undefined) {
if (value === undefined) throw new Error("value ist Pflicht");
return value.toUpperCase(); // narrowed auf string
}Narrowing auf null und undefined
TypeScript versteht eine Reihe von Prüfmustern und engt den Typ entsprechend ein.
function handle(x: string | null | undefined) {
// Strict: nur auf null prüfen
if (x === null) { /* x: null */ }
if (x !== null) { /* x: string | undefined */ }
// Strict: nur auf undefined prüfen
if (typeof x === "undefined") { /* x: undefined */ }
if (x !== undefined) { /* x: string | null */ }
// Loose-Equality: fängt beide auf einmal
if (x == null) { /* x: null | undefined */ }
if (x != null) { /* x: string */ }
// Truthy-Check: fängt auch "" — Vorsicht bei legalen leeren Strings
if (x) { /* x: string (aber "" ausgeschlossen!) */ }
}Das == null-Muster ist die einzige Stelle, an der == (statt ===) als idiomatisch gilt: Es prüft elegant gegen beide nullish-Werte gleichzeitig und ist ein dokumentierter Workflow im TypeScript-Handbook. Lint-Regeln wie eqeqeq haben dafür typischerweise eine Ausnahme ({ "null": "ignore" }).
Häufige Stolperfallen
Code ohne strictNullChecks ist ein Zeitbomben-Anti-Pattern.
Ohne den Flag versteckt der Compiler tausende potenzielle Nullable-Fehler. Jede neue Codezeile vergrößert das Risiko, dass sich ein Cannot read properties of undefined erst zur Laufzeit zeigt. Für neue Projekte ist strict: true (das strictNullChecks einschließt) Pflicht; für alte Projekte gibt es Migrationspfade über // @ts-expect-error-Markierungen und schrittweise Aktivierung.
== vs. === bei null und undefined — die einzige sinnvolle Ausnahme.
Sonst gilt === immer. Aber: x == null fängt sowohl null als auch undefined in einem einzigen Check ab — und genau das ist oft erwünscht. ESLint-Regel eqeqeq mit { "null": "ignore" } erlaubt das Muster gezielt.
?? 0 vs. || 0 — bei value = 0 unterschiedliches Verhalten.
0 || 0.5 ergibt 0.5, weil 0 falsy ist. 0 ?? 0.5 ergibt 0, weil 0 nicht nullish ist. Wer „Default nur bei fehlendem Wert" meint, muss ?? verwenden — sonst werden legale Werte wie 0, "" oder false still überschrieben.
Die !-Assertion lügt das Typsystem an.
value!.foo sagt dem Compiler, dass value nicht null ist — zur Laufzeit aber wird nicht geprüft. Ist die Annahme falsch, kommt der TypeError trotzdem. Verwende ! nur, wenn du eine echte Invariante hast, die der Compiler nicht sehen kann, und bevorzuge sonst echte Type Guards oder eine Exception bei Verstoß.
void ist NICHT dasselbe wie undefined-Return.
Eine Funktion mit Rückgabetyp void darf intern return undefined oder gar nicht returnen — aber Aufrufer dürfen den Rückgabewert nicht verwenden. (): undefined dagegen erzwingt, dass die Funktion explizit undefined zurückgibt und sich auch so verwenden lässt. Praktischer Unterschied: void-Callbacks akzeptieren jede beliebige Rückgabe (z. B. arr.forEach(x => arr2.push(x))), undefined-Callbacks nicht.
exactOptionalPropertyTypes trennt fehlend von undefined.
Mit dem Flag (seit TS 4.4) bedeutet name?: string nur noch „darf fehlen" — der Wert undefined ist explizit nicht erlaubt. Damit lassen sich Bugs vermeiden, in denen { name: undefined } beim Spread plötzlich existierende Properties überschreibt. Lohnt sich bei neuen Projekten, ist bei Migration aber laut, weil viele Bibliotheken die Unterscheidung nicht treffen.
delete obj.key erzeugt undefined-Property — nicht "weg".
Nach delete ist die Property zwar nicht mehr im Objekt (mit in nicht mehr auffindbar), der Zugriff obj.key liefert aber weiterhin undefined — wie bei jeder fehlenden Property. delete ist außerdem teuer, weil es V8-Hidden-Class-Optimierungen bricht. Besser: Property gar nicht erst setzen oder das Objekt unveränderlich neu konstruieren.
JSON.parse liefert null — nicht undefined.
JSON kennt nur null, kein undefined. Wer Backend-Antworten parst, bekommt für leere Felder null, auch wenn das interne Modell undefined erwartet. An der API-Grenze einmal normalisieren (value ?? undefined) erspart unzählige Edge-Cases tiefer im Code.
undefined ist (theoretisch) überschreibbar — zumindest historisch.
In alten JS-Engines (ES3, IE8) war undefined eine normale, schreibbare globale Variable. Defensive Bibliotheken nutzten deshalb void 0 als garantiert echtes undefined. Seit ES5 ist globalThis.undefined read-only — moderne Codebasen können undefined bedenkenlos verwenden. Im Funktions-Scope lässt sich undefined aber technisch immer noch als Parameter-Name überschatten — Stilrichtlinien verbieten das aus gutem Grund.
Weiterführende Ressourcen
Externe Quellen
- null and undefined – TypeScript Handbook
- strictNullChecks – tsconfig
- TypeScript 3.7 Release Notes (?., ??)
- exactOptionalPropertyTypes – tsconfig