null vs. undefined
null bedeutet:
Es gibt dieses Feld. Und es ist absichtlich leer.
undefined bedeutet:
Dieser Wert wurde nicht gesetzt. Oder dieses Konzept existiert an dieser Stelle nicht.
Das klingt nach Haarspalterei.
Ist es aber nicht.
Es ist einer der häufigsten Orte, an dem Business-Semantik unbemerkt in eine Lücke fällt.

null ist nicht einfach „nichts“
Abschnitt betitelt „null ist nicht einfach „nichts““In vielen Hochsprachen ist null bereits schwierig genug.
In JavaScript und TypeScript ist die Lage noch etwas spezieller, weil es zwei verschiedene „keine Wert“-Konzepte gibt:
null;undefined;Und beide bedeuten nicht dasselbe.
null ist ein expliziter Wert.
const deletedAt = null;Das sagt:
Das Feld ist vorhanden. Es wurde bewusst auf leer gesetzt.
undefined entsteht dagegen häufig, wenn etwas nicht initialisiert, nicht vorhanden oder nicht übergeben wurde.
let deletedAt;
console.log(deletedAt); // undefinedDas sagt eher:
Hier wurde kein Wert gesetzt.
Diese Unterscheidung ist nicht akademisch. Sie ist semantisch.
Die vereinfachte Regel
Abschnitt betitelt „Die vereinfachte Regel“Als pragmatische Arbeitsregel:
null: explizite Leerstelleundefined: nicht gesetzt, nicht vorhanden oder nicht geliefert
Beispiele:
type User = { id: string; name: string;
// Der Nutzer kann gelöscht sein. // Wenn nicht gelöscht, ist der Wert bewusst leer. deletedAt: Date | null;
// Dieses Feld ist optional. // Es muss nicht existieren. lastLogin?: Date;};deletedAt: null bedeutet:
Der Nutzer ist nicht gelöscht.
lastLogin: undefined oder fehlend bedeutet:
Zu diesem Nutzer liegt kein Login-Wert vor oder das Feld wurde nicht geliefert.
Das ist ein Unterschied.
Und dieser Unterschied sollte nicht zufällig entstehen.
Das Problem: stille Fehler
Abschnitt betitelt „Das Problem: stille Fehler“Wenn eine API null zurückgibt und der Frontend-Code nur auf undefined prüft, entsteht oft kein lauter Fehler.
Es passiert einfach das Falsche.
type UserDto = { assigned_to_user_id: string | null;};
function getAssigneeLabel(userId: string | undefined): string { if (userId === undefined) { return 'Unzugewiesen'; }
return `User ${userId}`;}Dann kommt von der API:
const dto = { assigned_to_user_id: null,};Und plötzlich ist null kein „Unzugewiesen“, sondern läuft durch eine Logik, die nie dafür gebaut wurde.
Kein Crash. Keine Exception. Nur falsches Verhalten.
Das ist gefährlicher als ein lauter Fehler.
Truthy/Falsy macht es nicht besser
Abschnitt betitelt „Truthy/Falsy macht es nicht besser“Eine beliebte Abkürzung ist:
if (value) { // Wert ist da}Das ist bequem.
Aber es vermischt unterschiedliche Fälle:
!!null; // false!!undefined; // false!!''; // false!!0; // false!!false; // falseDas Problem ist nicht, dass diese Auswertung falsch wäre.
Das Problem ist, dass sie zu grob ist.
Wenn 0 ein gültiger Wert ist, darf er nicht wie „kein Wert“ behandelt werden.
function formatCount(count: number | null): string { if (!count) { return 'Keine Einträge'; }
return `${count} Einträge`;}Das sieht harmlos aus.
Aber bei 0 ist die Frage fachlich:
Ist 0 ein gültiger Wert oder bedeutet 0 „nichts“?
Wenn 0 ein gültiger Wert ist, ist if (!count) falsch.
Besser:
function formatCount(count: number | null): string { if (count === null) { return 'Keine Daten'; }
return `${count} Einträge`;}Oder, wenn null und undefined gemeinsam behandelt werden sollen:
if (count == null) { // trifft null und undefined}Das ist einer der wenigen Fälle, in denen == null bewusst sinnvoll sein kann.
Aber dann bitte absichtlich.
Nicht aus Versehen.

Destructuring kann brechen
Abschnitt betitelt „Destructuring kann brechen“Ein weiterer Klassiker:
const user = null;
const { name } = user;Das crasht.
Cannot destructure property 'name' of 'user' as it is null.Dasselbe gilt für undefined.
const user = undefined;
const { name } = user;Auch das bricht.
Deshalb sieht man oft:
const { name } = user ?? {};Das verhindert den Crash.
Aber auch hier muss man vorsichtig sein.
Denn damit wird aus:
user ist nullplötzlich:
user ist leeres ObjektDas kann korrekt sein.
Es kann aber auch fachliche Semantik verdecken.
Wenn user === null bedeutet „kein Nutzer zugeordnet“, dann sollte diese Bedeutung vielleicht explizit behandelt werden:
if (user === null) { return 'Unzugewiesen';}
const { name } = user;Der Code ist länger.
Aber er sagt die Wahrheit.
Default Values greifen nur bei undefined
Abschnitt betitelt „Default Values greifen nur bei undefined“Das ist eine der gemeinsten JavaScript-Fallen.
Default Values greifen bei undefined.
Nicht bei null.
function greet(name = 'Gast') { return `Hallo ${name}`;}
greet(undefined); // "Hallo Gast"greet(null); // "Hallo null"Dasselbe beim Destructuring:
const user = { name: null,};
const { name = 'Gast' } = user;
console.log(name); // nullViele erwarten hier 'Gast'.
Aber der Default greift nicht, weil name vorhanden ist. Der Wert ist nur null.
Das ist exakt die Semantik:
undefined: Wert fehlt → Default greiftnull: Wert ist bewusst leer → Default greift nicht
Wenn du null wie „fehlend“ behandeln willst, brauchst du Nullish Coalescing:
const label = user.name ?? 'Gast';Das behandelt null und undefined.
Aber nicht '', 0 oder false.
Und genau deshalb ist ?? oft besser als ||.
const countLabel = count || 10; // 0 wird zu 10const countLabel = count ?? 10; // nur null/undefined wird zu 10
API, Domain und UI brauchen eine Entscheidung
Abschnitt betitelt „API, Domain und UI brauchen eine Entscheidung“Das eigentliche Problem entsteht nicht dadurch, dass null existiert.
Das Problem entsteht, wenn niemand entscheidet, was es bedeutet.
Eine API liefert:
assigned_to_user_id: null;Die Domain denkt:
assignee?: UserDie UI prüft:
if (!assignee) ...Und ein Mapper macht:
assigneeLabel: dto.assigned_to_user_id || 'Unzugewiesen';Jetzt ist die Semantik verteilt.
Ist der Nutzer nicht zugewiesen? Wurde das Feld nicht geladen? Ist die ID leer? Ist das ein API-Fehler? Ist es ein gültiger Zustand?
Niemand weiß es mehr genau.
Darum ist die wichtigste Regel:
Entscheidet pro Grenze, was
nullundundefinedbedeuten.
Zum Beispiel:
- API darf
nullliefern, wenn ein Feld explizit leer ist. - DTOs bilden die API ehrlich ab.
- Mapper übersetzen
nullin eine fachliche Bedeutung. - Domain-/ViewModels vermeiden unnötige Nullability.
- Komponenten bekommen möglichst eindeutige Werte.
null nicht verdammen
Abschnitt betitelt „null nicht verdammen“Man muss null nicht grundsätzlich verbieten.
null kann sehr sinnvoll sein.
Zum Beispiel:
type User = { deletedAt: Date | null;};Hier ist null gut:
Dieses Feld existiert immer. Aber der Nutzer ist nicht gelöscht.
Oder:
type Task = { assigneeId: string | null;};Auch das kann sinnvoll sein:
Eine Aufgabe kann bewusst niemandem zugewiesen sein.
Das ist eine fachliche Aussage.
Problematisch wird null, wenn es nur bedeutet:
Irgendwas ist halt leer. Viel Glück.
Dann wird es gefährlich.
Moderne Konzepte vermeiden null oft bewusst
Abschnitt betitelt „Moderne Konzepte vermeiden null oft bewusst“Viele modernere Modellierungsansätze versuchen, null so weit wie möglich zu vermeiden.
Nicht, weil null böse ist.
Sondern weil null Bedeutung versteckt.
Stattdessen modelliert man Zustände explizit.
Zum Beispiel mit Union Types:
type Assignee = { kind: 'assigned'; userId: string; name: string } | { kind: 'unassigned' };Dann muss niemand raten, was null bedeutet.
function getAssigneeLabel(assignee: Assignee): string { switch (assignee.kind) { case 'assigned': return assignee.name;
case 'unassigned': return 'Unzugewiesen'; }}Oder bei Ladezuständen:
type UserState = { status: 'idle' } | { status: 'loading' } | { status: 'loaded'; user: User } | { status: 'error'; message: string };Das ist oft besser als:
user: User | null;loading: boolean;error: string | null;Denn bei mehreren nullable Feldern entstehen schnell unmögliche Zustände:
{ user: null, loading: false, error: null,}Was bedeutet das?
Noch nicht geladen? Geladen, aber kein User? Fehler vergessen? Leerer Zustand?
Union Types machen solche Zustände explizit.

TypeScript hilft — wenn man es lässt
Abschnitt betitelt „TypeScript hilft — wenn man es lässt“TypeScript kann diese Unterschiede sichtbar machen.
Aber nur, wenn strictNullChecks aktiviert ist.
Mit strictNullChecks sind diese Typen unterschiedlich:
string;string | null;string | undefined;string | null | undefined;Das zwingt dich, eine Entscheidung zu treffen.
function formatName(name: string | null): string { return name.toUpperCase();}Das kompiliert dann nicht ohne Behandlung von null.
Besser:
function formatName(name: string | null): string { if (name === null) { return 'Unbekannt'; }
return name.toUpperCase();}Oder:
function formatName(name: string | null): string { return name?.toUpperCase() ?? 'Unbekannt';}Wichtig ist nicht die konkrete Schreibweise.
Wichtig ist: Der leere Fall wird sichtbar.
Eine praktische Regel für Frontends
Abschnitt betitelt „Eine praktische Regel für Frontends“Für Frontend-Architekturen hat sich diese Regel bewährt:
DTOs dürfen
nullenthalten. ViewModels sollten möglichst wenignullenthalten.
Warum?
DTOs bilden externe Realität ab.
Wenn die API null liefert, darf das DTO null zeigen.
type TaskDto = { assigned_to_user_id: string | null;};Aber das ViewModel sollte der UI möglichst keine Rätsel geben.
type TaskViewModel = { assigneeLabel: string; isAssigned: boolean;};Dann muss das Template nicht wissen, was null bedeutet.
<span>{{ task.assigneeLabel }}</span>Das ist langweilig.
Und genau deshalb gut.
null ist nicht grundsätzlich schlecht.
Aber null ist gefährlich, wenn es ohne klare Bedeutung durch das System wandert.
Der Unterschied zwischen null und undefined ist nicht akademisch:
nullist eine explizite Leerstelle.undefinedist nicht gesetzt oder nicht vorhanden.- Default Values greifen bei
undefined, aber nicht beinull. - Truthy/Falsy-Checks verwischen fachliche Unterschiede.
- Destructuring auf
nulloderundefinedbricht. - Union Types machen Zustände oft klarer als mehrere nullable Felder.
Die kurze Regel:
Nutze
null, wenn die Leerstelle fachlich gemeint ist. Nutzeundefined, wenn etwas nicht gesetzt oder nicht vorhanden ist. Und übersetze beides an klaren Grenzen in eindeutige Modelle.
Oder noch härter:
nullist erlaubt. Bedeutungslosesnullist ein Architekturleck.