开启 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 !== undefined | undefined |
x !== null | null |
x != null | null 和 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 值。