
TypeScript 中的 null 与 undefined语义辨析、统一判空与 strictNullChecks 实战【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址: https://gitcode.com/gh_mirrors/ty/typescript-book导读null与undefined是 JavaScript以及作为其超集的 TypeScript中的两个底层类型bottom types它们在语义上有着细微但重要的区别undefined表示尚未初始化null表示当前不可用。本文基于 typescript-book 仓库的 docs/javascript/null-undefined.md 展开系统讲解二者的语义差异、 null统一判空技巧、顶层变量检查、Node 风格回调约定、JSON 序列化差异并结合仓库中的 strictNullChecks 配置文档 与 类型系统文档 深入底层原理。读完本文你将掌握一套在 TypeScript 项目中安全、一致地处理空值的最佳实践。null 与 undefined两种底层类型的语义定位JavaScript以及 TypeScript存在两个底层类型null和undefined。它们被有意设计为表达不同的含义undefined表示某样东西尚未被初始化Something hasnt been initialized。null表示某样东西当前不可用Something is currently unavailable。例如一个声明了但从未赋值的变量值是undefined而一个函数参数明确表示这里没有错误时通常传null。从类型系统的角度看这两个字面量在strictNullChecks关闭时会被当作类似any的类型可以赋值给任何其他类型详见 docs/types/type-system.md 的 Special Types 一节var num: number; var str: string; // 关闭 strictNullChecks 时这两个字面量可以赋给任何类型 num null; str undefined;统一判空为什么推荐 null在实际开发中你往往同时需要处理null和undefined。JavaScript 的宽松相等运算符在这里有一个非常有趣的特性null和undefined只与彼此以及自身相等而不会与任何假值falsy value相等// null 和 undefined 只与自身和彼此 相等 console.log(null null); // true 理所当然 console.log(undefined undefined); // true 理所当然 console.log(null undefined); // true // 你不必担心假值会混过这个检查 console.log(0 undefined); // false console.log( undefined); // false console.log(false undefined); // false因此推荐使用 null来同时检查undefined或null。你通常并不需要在两者之间做出区分——绝大多数场景下把两者一并排除是正确且安全的选择function foo(arg: string | null | undefined) { if (arg ! null) { // 因为 ! 同时排除了 null 和 undefinedarg 在这里必然是 string } }也可以写成 undefined但 null更约定俗成、也更简短。这一点与仓库中 docs/javascript/equality.md 的 ProTip 完全呼应始终使用和!唯一的例外就是 null 检查——在普通场景下会做令人意外的类型强制转换如0 为true但在 null 检查这一场景中它恰好是唯一正确且简洁的选择。顶层全局变量的检查使用typeof前面推荐 null但有一个例外不要用它检查根级顶层/全局的变量。在严格模式下如果你直接引用一个未定义的变量foo会抛出ReferenceError异常整个调用栈随之展开unwind程序崩溃。你应该使用严格模式……实际上只要你使用了模块ES modulesTypeScript 编译器会自动为你插入use strict所以你不必显式声明。本书后续章节会详述这里先不展开。所以要判断一个变量在全局层级是否已定义标准做法是用typeofif (typeof someglobal ! undefined) { // 此时 someglobal 已可安全使用 console.log(someglobal); }typeof操作符即使作用于未声明的变量也不会抛错只会返回字符串undefined因此它是检查全局变量是否存在的唯一安全手段。限制显式使用undefined用类型注解替代TypeScript 给了你把结构文档化在类型上、而不是文档化在值上的机会。下面这种到处显式返回undefined的写法应当避免function foo(){ // 如果是某种情况 return {a:1,b:2}; // 否则 return {a:1,b:undefined}; }更好的做法是使用类型注解把b 可能不存在表达在类型层面function foo():{a:number,b?:number}{ // 如果是某种情况 return {a:1,b:2}; // 否则 return {a:1}; }b?: number这种可选属性optional property语法明确表达了b的值可能为undefined。在开启strictNullChecks后编译器会在你未做检查就使用可选属性时给出编译期错误如 Object is possibly undefined把运行期事故提前到编译期拦截详见 docs/options/strictNullChecks.md。在仓库示例代码中也能看到这一惯例的影子例如 code/types/typeGuard.ts 里的用户自定义类型守卫正是通过arg.foo ! undefined来缩小类型范围function isFoo(arg: any): arg is Foo { return arg.foo ! undefined; }Node 风格回调中的null约定Node 风格的回调函数如(err, somethingElse) { /* ... */ }在没有错误时通常把err置为null。实践中你一般只需要做真值truthy检查即可fs.readFile(someFile, utf8, (err,data) { if (err) { // 有错误处理之 } else { // 没有错误 } });在创建自己的 API 时为了与 Node 生态保持一致这种场景下使用null是合理的。不过坦诚地说对于你自己的 API更应该考虑Promise——在 Promise 模型下你根本不需要操心缺失的错误值通过.then与.catch分别处理成功与失败即可。不要用undefined表示有效性把结果无效编码为返回undefined是一种反模式。例如下面这个糟糕的函数function toInt(str: string) { return str ? parseInt(str) : undefined; }它把入参不合法与转换结果混在一个返回值里调用方只能靠 undefined来区分极容易出错。更好的写法是返回一个带有效性标志的结构也可以理解为一种简单的可辨识联合 / tagged union 思想function toInt(str: string): { valid: boolean, int?: number } { const int parseInt(str); if (isNaN(int)) { return { valid: false }; } else { return { valid: true, int }; } }这样调用方可以显式检查result.valid语义清晰、可读性强。仓库中关于可辨识联合的更多内容见 docs/types/discriminated-unions.md。JSON 与序列化null会被编码undefined会被剔除JSON 标准支持编码null但不支持undefined。对一个对象做 JSON 编码时值为null的属性会被包含在结果中保留其 null 值值为undefined的属性会被整体剔除。JSON.stringify({willStay: null, willBeGone: undefined}); // {willStay:null}由此可以得出两个实用推论基于 JSON 的数据库如 MongoDB 等通常支持null值但不支持undefined值。由于值为null的属性会被编码你可以在把对象传输到远端存储之前把某个属性显式置为null来表达清除该属性的意图。把属性值设为undefined可以节省存储与传输成本——因为属性名根本不会被编码。但代价是这会混淆清除值与值缺失的语义让数据消费者难以判断字段到底是不存在还是被有意清空。因此用null表达清除意图用undefined表达字段缺席是一套在序列化场景下值得遵守的约定。类型系统视角strictNullChecks 与断言操作符原文档讨论的是值层面的判空技巧而 TypeScript 还提供编译器开关从类型系统层面帮你管理空值。默认情况下strictNullChecks: falsenull和undefined可以赋值给所有类型let foo: number 123; foo null; // 默认 OK foo undefined; // 默认 OK这是为了模拟很多人编写 JavaScript 的实际方式。但 TypeScript 允许你通过strictNullChecks编译器选项对应 tsconfig.json 中的strictNullChecks: true它也是strict: true的组成部分见 docs/project/tsconfig.md对什么可以、什么不可以赋值为null/undefined做出显式声明。开启后null和undefined被视为彼此不同的类型let foo undefined; foo null; // NOT Okay开启 strictNullChecks 后报错一个典型场景接口的可选属性在开启严格检查后会被强制要求先判空再使用。例如定义interface Member { name: string; age?: number }后getMember() .then((member: Member) { const stringifyAge member.age.toString() // 严格模式下报错Object is possibly undefined })这种undefined 是一切罪恶之源的运行期错误Cannot read property toString of undefined在严格模式下会被提前到编译期拦截。这正是 docs/options/strictNullChecks.md 所强调的核心价值。针对编译器无法自行证明非空的情况TypeScript 还提供两种断言操作符非空断言!后缀表达式操作符用于断言操作数非 null 且非 undefined。注意它只是断言就像类型断言一样确保值不为 null 的责任在你身上function validateEntity(e?: Entity) { /* 若 e 为 null 则抛异常 */ } function processEntity(e?: Entity) { validateEntity(e); let a e.name; // TS ERRORe 可能为 null let b e!.name; // OKAY我们断言 e 非 null }明确赋值断言!属性/变量后缀当属性在构造函数之外完成初始化、或变量在函数调用后才被赋值时用它告知编译器我确实会初始化它class C { foo!: number; // 明确赋值断言我在构造函数之外初始化它 constructor() { this.initialize(); } initialize() { this.foo 0; } }与所有断言一样这些操作符都是让编译器信任你请务必只在确实保证非空/已初始化时使用。最终思考社区观点与你的选择关于null与undefined的使用业界存在不同流派TypeScript 团队自身不使用null其编码规范中明确如此并且这没有造成任何问题Douglas Crockford则认为null是个坏主意主张大家统一只用undefined但NodeJS 风格的代码库普遍以null作为 Error 参数的惯例因为它准确表达了当前不可用的语义。作者个人的立场是不必执着于区分两者——大多数项目混用了持有不同观点的库与其争论不如统一用 null把两者一并排除。这个务实的建议也构成了本文贯穿始终的方法论判空统一用 null/! null全局变量用typeof检查用类型注解可选属性、联合类型替代显式undefined返回值序列化场景中用null表达清除用undefined表达缺席开启strictNullChecks让编译器替你兜住空值访问的错误。【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址: https://gitcode.com/gh_mirrors/ty/typescript-book创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考