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.

  • undefined entsteht automatisch, wenn eine Variable deklariert aber nicht zugewiesen wurde, eine Funktion ohne return endet, ein Funktions-Parameter fehlt, oder eine Objekt-Property nicht existiert.
  • null ist ein bewusster Wert — vom Entwickler explizit gesetzt, um „hier steht absichtlich nichts" auszudrücken. JavaScript erzeugt null nie von selbst (mit der berühmten Ausnahme typeof null === "object").
ts
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 fehlt

Konvention 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.

ts
// 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: TypeError

Array.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.

ts
// 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:

  • null und undefined sind 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 automatisch string | 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:

TypBedeutungTypischer Einsatz
string | undefinedWert fehlt oder ist noch nicht gesetztOptionale Parameter, fehlende Map-Einträge, find()-Ergebnisse
string | nullWert ist explizit als „leer" markiertDomänen-Modelle, DB-Spalten mit NULL, geleerte Formularfelder
string | null | undefinedBeide Zustände sind möglichAPI-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.

ts
// 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:

ts
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 };    // OK

exactOptionalPropertyTypes (seit TS 4.4): Ist dieses strikte Flag aktiv, akzeptiert A nur das Fehlen der Property, nicht den expliziten Wert undefinedname?: string wird dann zu echtem „optional", ohne den Trostpreis | undefined.

ts
// 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.

ts
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 ||.

ts
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);
Output
0.5 0 Anon  true false

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

ts
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.

ts
function shout(value: string | undefined) {
  console.log(value!.toUpperCase());
  //               ^ — Compiler glaubt: value ist string
}

shout(undefined); // Laufzeit: TypeError: Cannot read properties of undefined

Legitime 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 beforeEach Werte 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:

ts
// 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.

ts
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

/ Weiter

Zurück zu Primitive Typen

Zur Übersicht