Menu

TypeScript null 和 undefined:判空检查与 ?.、?? 运算符

开启 strictNullChecks 后,null 和 undefined 是独立的类型,TypeScript 会要求你处理它们。本文讲解如何检查它们、可选链(?.)、双问号(??)和 ??=,以及可选属性和 | undefined 的区别。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

开启 strictNullChecks(它属于 strict 的一部分)后,null 和 undefined 就是各自独立的类型。string 永远不可能是 null;一个可能缺失的值必须在类型中写明,例如 string | null,而 TypeScript 会要求你在使用这个值之前处理这种情况。

检查之后,编译器知道 name 是 string,所以可以调用 .split。本页其余部分介绍做这种检查的各种方法,以及让它变得简短的运算符。

strictNullChecks 与“可能为 null”错误

当类型中包含 null 或 undefined 时,TypeScript 拒绝把这个值当作一定存在来使用:

index.ts(3,22): error TS18047: 'name' is possibly 'null'.

undefined 对应的错误是 TS18048:'x' is possibly 'undefined'。没有 strictNullChecks 时,null 和 undefined 可以出现在任何类型中,这段代码能编译通过,然后在 name 第一次为 null 时于运行时因 TypeError 崩溃。这类 bug 正是要保持 strict 开启的主要原因。

日常代码中这些类型的来源:

来源类型
arr.find(...)T | undefined
map.get(key)V | undefined
可选属性 p?: T读取时为 T | undefined
可选参数 x?: T函数内部为 T | undefined
str.match(re)RegExpMatchArray | null
JSON.parse(text)any,因此什么都不检查

检查 null 和 undefined

下面每一种检查都会在代码块内收窄类型。根据你想排除的内容选择合适的检查。

检查从类型中去掉
x !== undefinedundefined
x !== nullnull
x != nullnull 和 undefined
typeof x !== "undefined"undefined
if (x)null 和 undefined,同时还会跳过 0、""、false、NaN 这些值

== null 是唯一一个惯用宽松相等的地方:它只对 null 和 undefined 为真,对其他值都不成立。真值检查会把空字符串也当作缺失,这往往是个 bug。

可选链:?.

如果 a 不是 null 或 undefined,a?.b 就读取 b,否则停下来并返回 undefined。同一个运算符也适用于索引 a?.[i] 和调用 fn?.()。

bob.address?.city 的类型是 string | undefined:可选链会给结果加上 undefined,所以通常要和 ?? 搭配使用。运行时的行为就是普通 JavaScript;短路求值的细节见 可选链。

双问号:??

除非 a 是 null 或 undefined,否则 a ?? b 返回 a;如果是,就返回 b。它取代了旧的 a || b 写法,后者还会丢弃 0、""、false 和 NaN:

左侧的值left || "d"left ?? "d"
null"d""d"
undefined"d""d"
0"d"0
"""d"""
false"d"false
NaN"d"NaN

在类型上,?? 从左侧去掉 null 和 undefined,再把剩下的部分和右侧组成联合类型,所以 scores.get("Linus") ?? 0 可以赋值给 number。

空值赋值:??=

只有当 a 是 null 或 undefined 时,a ??= b 才会把 b 赋给 a。它的同类 ||= 和 &&= 分别在左侧为假值或真值时赋值。

retries: 0 在 ??= 之后保留下来,而空的 label 被 ||= 替换了。在 opts.retries ??= 3 之后,TypeScript 在函数的其余部分把 opts.retries 收窄为 number。

可选属性与 | undefined

nickname?: string 和 nickname: string | undefined 读取时的类型一样,但区别在于这个键是否必须存在:

当调用方可以省略这个属性时用 ?;当你希望每个调用方都显式传入它(即使值是 undefined)时用 | undefined。exactOptionalPropertyTypes 选项(不属于 strict)会让 ? 更严格:此时 nickname?: string 会拒绝 { nickname: undefined },只接受缺失的键或字符串。可选的函数参数(x?: number)和可选属性的行为一样:在函数内部,x 是 number | undefined。

用 null 还是 undefined

TypeScript 并不强制你选择,但在一个代码库里两者混用,意味着每次检查都要处理两种情况。一种常见的约定:

  • 在你自己的类型中,用 undefined(以及可选属性)表示“未设置”。这是 JavaScript 默认产生的值:缺失的属性、省略的实参、find 和 Map.get 没有找到时。
  • 在 API 给你 null 的地方接受 null:JSON 没有 undefined,String.prototype.match 和很多 DOM 方法会返回 null。
  • 当一个值两者都有可能时,用 == null 检查。

数组索引是唯一的漏洞:即使索引越界,users[5] 的类型也是元素类型。noUncheckedIndexedAccess 选项(不属于 strict)会给每次索引访问加上 | undefined,让编译器连这种情况也能发现。

常见问题

TypeScript 中的双问号是什么意思?

a ?? b 是来自 JavaScript 的空值合并运算符。除非 a 是 null 或 undefined,否则它返回 a;如果是,就返回 b。和 || 不同,它会保留 0、""、false 等其他假值。TypeScript 会从左侧的类型中去掉 null 和 undefined,所以当 a 是 string | undefined 时,a ?? "x" 是 string。

TypeScript 中如何判断一个值是否为 undefined?

直接比较:if (value !== undefined) { ... }。TypeScript 会在代码块内收窄类型。要在一次检查中同时排除 null 和 undefined,使用 value != null(宽松相等),这是唯一一个惯用 ==/!= 的地方。真值检查(if (value))也能收窄类型,但会跳过 0、"" 和 false。

“Object is possibly undefined”是什么意思?

错误 TS18048('x' is possibly 'undefined')和 TS18047('x' is possibly 'null'),以及值没有简单名字时(例如 getUser().address.city)的 TS2532(Object is possibly 'undefined'),都来自 strictNullChecks:类型中包含 undefined 或 null,而代码却把这个值当作不可能缺失来使用。先检查它、使用可选链(x?.name)、用 ?? 提供默认值,或者在值确实不可能缺失时修改类型。

TypeScript 中 null 和 undefined 有什么区别?

它们是两个独立的类型,各自只有一个值。JavaScript 用 undefined 表示从未设置过的东西(缺失的属性、省略的实参、对不存在的键调用 Map.get),而 API 用 null 表示有意的“没有值”(JSON、很多 DOM 方法)。TypeScript 分别跟踪它们,所以 string | null 不接受 undefined。很多代码库在自己的代码中统一使用 undefined,只在边界处接受 null。

可选属性和 | undefined 是一样的吗?

不完全一样。name?: string 表示这个属性可以完全不存在,读取它得到 string | undefined。name: string | undefined 表示属性必须存在,即使它的值是 undefined。开启 exactOptionalPropertyTypes 后,name?: string 也不再接受显式的 undefined 值。

Coddy 编程语言插图

和 Coddy 一起学编程

开始