null vs. undefined
null 表示:
这个字段存在,而且它被明确地设置为空。
undefined 表示:
这个值没有被设置,或者这个概念在这里根本不存在。
听起来像是在咬文嚼字。
其实不是。
这是 Business Semantics 最容易悄悄掉进空洞里的地方之一。

null 不只是“什么都没有”
Section titled “null 不只是“什么都没有””在很多高级语言里,null 已经够麻烦了。
JavaScript 和 TypeScript 更特别,因为它们有两个不同的“没有值”概念:
null;undefined;而它们的语义不同。
null 是一个显式值。
const deletedAt = null;它表达的是:
这个字段存在,并且被有意识地设置为空。
undefined 则经常出现在一个值尚未初始化、不存在或没有被传入的情况下。
let deletedAt;
console.log(deletedAt); // undefined它更接近:
这里没有设置任何值。
这不是学术区别,而是语义区别。
一个简化但实用的规则
Section titled “一个简化但实用的规则”可以先用下面这条工作规则:
null:显式的空值undefined:未设置、不存在或未提供
例如:
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 表示:
用户没有被删除。
而 lastLogin: undefined 或字段缺失,则意味着:
当前没有这个 Login Value,或者它根本没有被提供。
这是两个不同的事实。
这种区别不应该偶然产生。
真正危险的是静默错误
Section titled “真正危险的是静默错误”如果 API 返回 null,但 Frontend 只检查 undefined,通常不会出现一个非常响亮的异常。
程序只是悄悄做错事。
type UserDto = { assigned_to_user_id: string | null;};
function getAssigneeLabel(userId: string | undefined): string { if (userId === undefined) { return 'Unzugewiesen'; }
return `User ${userId}`;}然后 API 返回:
const dto = { assigned_to_user_id: null,};于是 null 不再表示“未分配”,而会穿过一段从未为它设计过的逻辑。
没有 Crash。 没有 Exception。 只有错误行为。
这种错误反而比响亮失败更危险。
Truthy / Falsy 也不会让事情更好
Section titled “Truthy / Falsy 也不会让事情更好”一个常见捷径是:
if (value) { // Wert ist da}很方便。
但它把多个不同情况混在一起:
!!null; // false!!undefined; // false!!''; // false!!0; // false!!false; // false问题不是 JavaScript 的结果不正确。
而是判断太粗。
如果 0 是一个有效值,就不能把它当成“没有值”。
function formatCount(count: number | null): string { if (!count) { return 'Keine Einträge'; }
return `${count} Einträge`;}这里真正的业务问题是:
0是一个有效结果,还是意味着“无值”?
如果 0 是有效值,if (!count) 就错了。
更明确的写法:
function formatCount(count: number | null): string { if (count === null) { return 'Keine Daten'; }
return `${count} Einträge`;}如果你明确希望同时处理 null 与 undefined:
if (count == null) { // trifft null und undefined}这是少数 == null 可以被有意识使用的场景。
重点是:要有意识。
不是碰巧。

Destructuring 也可能直接崩掉
Section titled “Destructuring 也可能直接崩掉”另一个经典例子:
const user = null;
const { name } = user;这会 Crash。
Cannot destructure property 'name' of 'user' as it is null.undefined 也一样。
const user = undefined;
const { name } = user;同样会失败。
所以常见的防御写法是:
const { name } = user ?? {};它能避免 Crash。
但也需要小心。
因为这样会把:
user ist null悄悄转换成:
user ist leeres Objekt这可能完全合理。
也可能把业务语义盖住。
如果 user === null 的含义是“没有分配用户”,那更好的代码也许是显式处理:
if (user === null) { return 'Unzugewiesen';}
const { name } = user;代码更长。
但它说出了事实。
Default Value 只对 undefined 生效
Section titled “Default Value 只对 undefined 生效”这是 JavaScript 里非常容易踩的坑之一。
Default Value 会在 undefined 时生效。
不会在 null 时生效。
function greet(name = 'Gast') { return `Hallo ${name}`;}
greet(undefined); // "Hallo Gast"greet(null); // "Hallo null"Destructuring 也一样:
const user = { name: null,};
const { name = 'Gast' } = user;
console.log(name); // null很多人会期待这里得到 'Gast'。
但 Default 不会触发,因为 name 这个字段存在,只是其值为 null。
这恰好体现了语义差别:
undefined:值缺失 → Default 生效null:值明确为空 → Default 不生效
如果你希望把 null 也视为“缺失”,可以使用 Nullish Coalescing:
const label = user.name ?? 'Gast';它会处理 null 和 undefined。
但不会误伤 ''、0 或 false。
也因此,很多场景下 ?? 比 || 更合适。
const countLabel = count || 10; // 0 wird zu 10const countLabel = count ?? 10; // nur null/undefined wird zu 10
API、Domain 和 UI 必须做出语义决定
Section titled “API、Domain 和 UI 必须做出语义决定”真正的问题不是 null 存在。
问题是没有人决定它代表什么。
API 返回:
assigned_to_user_id: null;Domain 里写:
assignee?: UserUI 里检查:
if (!assignee) ...Mapper 再写:
assigneeLabel: dto.assigned_to_user_id || 'Unzugewiesen';现在语义已经散落在整个系统中。
到底是“没有分配用户”? 字段没有加载? ID 是空的? API 出错? 还是这是完全合法的状态?
很快就没人能准确回答。
因此最重要的规则是:
在每一条边界上明确决定
null与undefined的含义。
例如:
- API 可以用
null表示“字段存在,但明确为空” - DTO 忠实反映 API Contract
- Mapper 把
null翻译成业务含义 - Domain Model / ViewModel 尽量避免不必要的 Nullability
- Component 尽量得到语义明确的值
不需要把 null 当成敌人
Section titled “不需要把 null 当成敌人”没有必要全面禁止 null。
null 可以非常有用。
例如:
type User = { deletedAt: Date | null;};这里的 null 很清晰:
这个字段总是存在,但当前用户没有被删除。
或者:
type Task = { assigneeId: string | null;};这同样可以很合理:
一项 Task 可以明确处于“无人分配”的状态。
这是业务语义。
真正危险的是 null 只表示:
反正这里是空的,祝你好运。
现代建模经常有意识地减少 null
Section titled “现代建模经常有意识地减少 null”很多较新的建模方式会尽量减少 null。
不是因为 null 本身邪恶。
而是因为 null 很容易隐藏含义。
一种替代方式是显式建模状态。
例如 Union Type:
type Assignee = { kind: 'assigned'; userId: string; name: string } | { kind: 'unassigned' };这样就不需要猜 null 的含义。
function getAssigneeLabel(assignee: Assignee): string { switch (assignee.kind) { case 'assigned': return assignee.name;
case 'unassigned': return 'Unzugewiesen'; }}加载状态也是一样:
type UserState = { status: 'idle' } | { status: 'loading' } | { status: 'loaded'; user: User } | { status: 'error'; message: string };这通常比下面的形式更清晰:
user: User | null;loading: boolean;error: string | null;因为多个 nullable 字段很容易产生无法解释的组合:
{ user: null, loading: false, error: null,}这代表什么?
还没加载? 加载完成,但没有 User? 错误被忘了? 还是合法的 Empty State?
Union Type 会迫使这些状态变得显式。

TypeScript 可以帮忙——前提是你允许它帮
Section titled “TypeScript 可以帮忙——前提是你允许它帮”TypeScript 能让这些区别在编译期变得可见。
但前提是打开 strictNullChecks。
启用后,下面这些类型彼此不同:
string;string | null;string | undefined;string | null | undefined;这迫使代码作出决定。
function formatName(name: string | null): string { return name.toUpperCase();}在没有处理 null 的情况下,它不会通过编译。
更好的写法可能是:
function formatName(name: string | null): string { if (name === null) { return 'Unbekannt'; }
return name.toUpperCase();}或者:
function formatName(name: string | null): string { return name?.toUpperCase() ?? 'Unbekannt';}具体写法不是重点。
重点是:空值情况被迫显式出现。
前端架构中的一个实用规则
Section titled “前端架构中的一个实用规则”一个很实用的经验是:
DTO 可以包含
null。ViewModel 应该尽量减少null。
为什么?
DTO 表达外部现实。
如果 API 返回 null,DTO 就应该诚实地表示 null。
type TaskDto = { assigned_to_user_id: string | null;};但 ViewModel 应该尽可能不给 UI 留谜题。
type TaskViewModel = { assigneeLabel: string; isAssigned: boolean;};这样 Template 根本无需知道 null 在 API 中代表什么。
<span>{{ task.assigneeLabel }}</span>这很无聊。
也正因为如此,它很好。
null 并不天然有问题。
真正危险的是一个没有明确含义的 null 在系统里一路流动。
null 与 undefined 的差异并不学术:
null是显式空值undefined表示未设置或不存在- Default Value 对
undefined生效,但不会对null生效 - Truthy / Falsy Check 会模糊业务差异
- 对
null或undefined做 Destructuring 会失败 - Union Type 往往比多个 nullable 字段更清晰
简短规则:
当“空”本身具有业务含义时使用
null;当值尚未设置或不存在时使用undefined。并在清晰的边界上,把两者翻译成语义明确的模型。
再直接一点:
null可以存在。没有意义的null才是架构泄漏。