TypeScript 类型深入探究 - 第 2 部分:值的缺失

2025-06-10

TypeScript 类型深入探究 - 第 2 部分:值的缺失

本文最初发表于Barbarian Meets Coding

TypeScript 是 JavaScript 的一个更现代、更安全的版本,它席卷了 Web 开发领域。它是 JavaScript 的一个超集,添加了额外的特性、语法糖和静态类型分析,旨在提高您的工作效率并扩展 JavaScript 项目。这是系列文章的第二部分,我们将探索 TypeScript 全面的类型系统,并学习如何利用它构建非常健壮且易于维护的 Web 应用程序

还没看过这系列的第一部分吗?如果你还没看过,不妨看看。里面有很多有趣又实用的内容。

JavaScript 和价值的缺失

null常被称为“十亿美元的错误” 。正如Tony Hoare在 1965 年首次在 ALGOL 中提出它时所说:

我称之为“十亿美元级错误”。它指的是1965年空引用的发明。当时,我正在设计面向对象语言(ALGOL W)中第一个全面的引用类型系统。我的目标是确保所有引用的使用都绝对安全,并由编译器自动执行检查。但我无法抗拒引入空引用的诱惑,仅仅因为它太容易实现了。这导致了无数的错误、漏洞和系统崩溃,在过去的四十年里,可能已经造成了数十亿美元的损失。

托尼·霍尔和十亿美元的错误

让我们用一个简单的例子来说明这种痛苦(尽管我很确定你可能很熟悉它并且已经经历过很多次了)。

想象一下,我们有这样一个函数,可以让我们消灭邪恶的敌人:

// Destroy Thy Enemies!!
function attack(target) {
  target.hp -= 10;
}
Enter fullscreen mode Exit fullscreen mode

嗯……现在是圣诞节。充满欢乐、幸福和爱的节日。所以我们换个更合适的例子吧:

// Love!!!!!
function hug(target) {
  target.happiness += 1000;
}
Enter fullscreen mode Exit fullscreen mode

现在好多了!想象一下,我们想利用这个hug功能在世界各地传播爱心:

// Love!!!!!
function hug(target) {
  target.happiness += 1000;
}

const sadJaime = {name: 'jaime', happiness: 0};
hug(sadJaime); // => Wihooo! ❤️
Enter fullscreen mode Exit fullscreen mode

太好了!一切都按计划进行。我们让一个人更开心了。耶!但如果我们的样本再复杂一点呢?

假设我们想拥抱所有人,比如世界上的每个人。我们有一张神奇的地图,上面已经预先填充了(可能是圣诞老人做的),上面写着地球上现在活着的每个人。所以我们验证一下,它确实像宣传的那样有效:

// Love!!!!!
function hug(target) {
  target.happiness += 1000;
}

const sadJaime = magicMap.get("Jaime"); // that's me of course
hug(sadJaime); // => Wihooo! ❤️
Enter fullscreen mode Exit fullscreen mode

太棒了!但如果我们要找的人根本不存在,或者现在不在地球上活着怎么办?

// Love!!!!!
function hug(target) {
  target.happiness += 1000;
}

// returns undefined because Eleanor is in The Good Place
const eleanor = magicMap.get("Eleanor Shellstrop"); 

// brace yourself
hug(eleanor); // => 💥💥💥 Much explosion. 
// Cannot read property happiness of undefined
Enter fullscreen mode Exit fullscreen mode

是的。问题就在这里。JavaScript 和 Java、Ada 以及 ALGOL 一样,也因 的存在而遭受同样的困扰null。不过,JavaScript 的情况可能更糟,因为我们有不止一种,而是两种方法来表示值的缺失:nullundefined

Brendan Eich 戴着太阳镜,将 null 和 undefined 放入 JavaScript 中,感觉棒极了

在 JavaScript 中,这两个关键字都表示值缺失。两者都会导致空引用异常1,就像上面示例中描述的那样。那么它们之间有什么区别呢?

有几种:

  • 据我所知,没有任何原生浏览器 API 会返回null。对于 Web 平台来说,始终缺少值undefined
  • 按照惯例,社区通常将 表示null为缺乏价值,而undefined表示尚未定义的内容。开发人员可以使用 来null表示故意缺乏价值,而 则undefined表示 Web 平台正在告诉您某些内容尚未定义。null客户端和服务器之间进行互操作时,API 也可能生成 (例如,Java 将 JSON 发送到 JavaScript 前端)。实际上,JSON 规范仅支持null和 ,而不支持undefined
  • 默认参数在给定undefined或时会有不同的表现null。将undefined或 传递给函数,函数将使用你提供的默认值。如果传递null,函数会很乐意使用它而不是默认值。(这非常危险)

null对于以上所有问题,我都会尽量避免,并undefined在平台产生问题时进行处理。但即便如此,有时你别无选择,只能绕过nullundefined。它们不可避免地会导致空引用异常,以及非常经典的undefined is not a function ……

那么,TypeScript 如何帮助我们解决这个问题?

TypeScript 和价值的缺失

因此,TypeScript 没有两种方式来表示值的缺失。它有四种

Anders Hejlsberg 戴着太阳镜,感觉棒极了,他把 null、undefined、void 和 never 都写进了 TypeScript

表示价值缺失的四种方式是:

  • undefined就像null在 JavaScript 中一样,因为 TypeScript 毕竟是 JavaScript 的超集
  • void表示不返回任何内容的函数的返回类型
  • never表示永不返回的函数的返回类型(例如,它会引发异常)

哇!四个而不是两个??这有什么好处吗?答案是肯定的。不是因为表示值缺失的方式有很多,而是因为在 TypeScript 中nullundefined也是类型

所以呢?

让我们看一下前面的例子。我们定义一个Target接口来表示任何具有happiness属性的事物(人类、宠物或怪物只要可以被拥抱就行):

interface Target {
  happiness: number;
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以在我们的hug函数中使用它了:

// Love!!!!!
function hug(target: Target) {
  target.happiness += 1000;
}
Enter fullscreen mode Exit fullscreen mode

如果我们像之前那样尝试拥抱悲伤的 Jaime,一切都会好起来的:

// Love!!!!!
function hug(target: Target) {
  target.happiness += 1000;
}

const sadJaime = magicMap.get("Jaime"); // that's me of course
hug(sadJaime); // => Wihooo! ❤️
Enter fullscreen mode Exit fullscreen mode

但是如果我们尝试拥抱埃莉诺会怎么样?

// Love!!!!!
function hug(target: Target) {
  target.happiness += 1000;
}

// returns undefined because Eleanor is in The Good Place
const eleanor = magicMap.get("Eleanor Shellstrop"); 

hug(eleanor); // => 💥 Argument of type 'undefined' is not assignable to parameter of type 'Target'.
Enter fullscreen mode Exit fullscreen mode

我们收到错误!我们收到一个编译时错误。也就是说,只要我们输入上面的代码,TypeScript 编译器就会告诉我们哪里出错了。因为该函数hug需要 a Target,而我们传入的undefined是一个完全不同的类型,它不具备 的任何特性Target,所以 TypeScript 编译器会介入并提供帮助。

这就是 TypeScript 如何利用nullundefined类型来防止你遇到空引用异常以及可怕的百万美元错误。是不是很棒?

严格空值检查

我们刚刚看到的 TypeScript 会阻止你使用 null 或 undefined 来自找麻烦,这种行为被称为严格空值检查。它是 TypeScript 编译器的一项功能,于 TypeScript 2.0 版本中引入,需要在 tsconfig 文件中通过 stricNullChecks 设置在启用它。通常,强烈建议你尽可能严格地配置 TypeScript。这样,你就能充分利用 TypeScript 的类型系统,并编写更安全、更易于维护的应用程序。

只是为了好玩。我们该如何重写上面的函数,让 fornullundefinedand 等价于 JavaScript 版本呢?一种方法如下:

// Love!!!!!
function hug(target: Target | undefined | null) {
  target.happiness += 1000;
}
Enter fullscreen mode Exit fullscreen mode

因此,我们明确地告诉 TypeScript,该函数的参数可以是Targetundefined或者null通过使用类型联合。或者:

// Love!!!!!
function hug(target?: Target | null) {
  target.happiness += 1000;
}
Enter fullscreen mode Exit fullscreen mode

其中我们使用一个可选参数,使用target?的简写符号Target | undefined

使用 Null 或 Undefined

在某些情况下,你仍然需要使用nullundefined。假设你正在使用一个用 JavaScript 实现的第三方库,它不太关心返回nullundefined。你可能有一位强大的战士,正要用他的剑砍一些土豆作为圣诞晚餐,所以你这样写:

const warrior = tavern.hireWarrior({goldCoins: 2});

if (warrior !== null 
    && warrior !== undefined
    && warrior.sword !== null 
    && warrior.sword !== undefined){
  warrior.sword.slash();
}
Enter fullscreen mode Exit fullscreen mode

您使用的战士雇佣 API 是用 JavaScript 编写的,您无法确定它是否返回战士,或者战士是否有剑,因此为了安全起见,您被迫编写一堆空/未定义的检查。

TypeScript(>=3.7)有一个替代的、更简洁的版本,可以替代上面的模式。可选链式运算符?.可以让我们重写上面的例子:

warrior?.sword?.slash();
// The ? is often called the Elvis operator
// because this ?:-p
Enter fullscreen mode Exit fullscreen mode

也就是说,只有和 her既不为 undefined 也不为 null时,我们才会调用该slash方法。在我看来,这是一个更好的选择。warriorsword

解决在声名狼藉的酒馆雇佣战士的问题的另一种方法是,手头有一个默认值,以便在出现问题时使用。因此,为了确保上述削减能够发生,我们可以采用始终确保强大的战士确实存在的方法:

let warrior = tavern.hireWarrior({goldCoins: 2});
if (!warrior) {
  warrior = new Warrior('Backup Plan Joe');
}
if (!warrior.sword) warrior.equip(new RustySword());

warrior.sword.slash();
Enter fullscreen mode Exit fullscreen mode

TypeScript 还提供了检查这是否为空以及是否分配其他值模式的替代方法:空值合并运算符 ??

我们可以??简化上面的例子:

let warrior = tavern.hireWarrior({goldCoins: 2}) 
              ?? new Warrior('Backup plan Joe');

if (!warrior.sword) warrior.equip(new RustySword());

warrior.sword.slash();
Enter fullscreen mode Exit fullscreen mode

不错吧??.and??运算符使得解决 TypeScript 中值缺失的问题变得更容易。

可选链式调用空值合并运算符作为 ES2020 的一部分获得批准,现已成为 JavaScript 语言的官方特性。太棒了!

有没有更好的方法来模拟价值的缺失?

一种非常酷的建模缺失值的方法来自函数式编程中以Maybemonad 的形式。在本系列的后续文章中,当我们深入探究泛型的奥秘时,我们将重新讨论这个主题,并了解 TypeScript 如何支持这些函数式编程模式。

总之

nullundefined可能对你的 JavaScript 程序造成严重破坏。它们可能会在运行时随意破坏程序,或者迫使你编写大量冗长的保护原因和防御性代码,这些代码可能会非常冗长,并掩盖你的核心业务逻辑。

在 TypeScript 中null, 和undefined也是类型。结合严格的空值检查,您可以在编译时保护程序免受空引用异常的影响。这样,一旦您在运行时犯了一个可能导致 bug 的错误,您就可以立即得到反馈并立即修复。

当需要使用nullTypeScript undefined3.7 和 ES2020 时,有两个功能可以减轻您的困扰。可选链式调用和空值合并运算符可以在处理 null 和 undefined 时节省大量的输入。

今天就到这里。希望你喜欢这篇文章,并且学到了一些新东西。下次再见,保重,祝你拥有美好的一天。


  1. 在 JavaScript 中,空引用异常被视为更通用的 TypeError。与其他语言(例如 C# 或 Java)不同,JavaScript 中没有仅适用于空引用异常的特定错误(NullReferenceException)  。↩

鏂囩珷鏉ユ簮锛�https://dev.to/vintharas/typescript-types-deep-dive-part-2-the-absence-of-value-1e8b
PREV
我实现了你那些愚蠢的应用程序想法!
NEXT
2019 年我最喜欢的书