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;
}
嗯……现在是圣诞节。充满欢乐、幸福和爱的节日。所以我们换个更合适的例子吧:
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
现在好多了!想象一下,我们想利用这个hug
功能在世界各地传播爱心:
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
const sadJaime = {name: 'jaime', happiness: 0};
hug(sadJaime); // => Wihooo! ❤️
太好了!一切都按计划进行。我们让一个人更开心了。耶!但如果我们的样本再复杂一点呢?
假设我们想拥抱所有人,比如世界上的每个人。我们有一张神奇的地图,上面已经预先填充了(可能是圣诞老人做的),上面写着地球上现在活着的每个人。所以我们验证一下,它确实像宣传的那样有效:
// Love!!!!!
function hug(target) {
target.happiness += 1000;
}
const sadJaime = magicMap.get("Jaime"); // that's me of course
hug(sadJaime); // => Wihooo! ❤️
太棒了!但如果我们要找的人根本不存在,或者现在不在地球上活着怎么办?
// 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
是的。问题就在这里。JavaScript 和 Java、Ada 以及 ALGOL 一样,也因 的存在而遭受同样的困扰null
。不过,JavaScript 的情况可能更糟,因为我们有不止一种,而是两种方法来表示值的缺失:null
和undefined
。
在 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
在平台产生问题时进行处理。但即便如此,有时你别无选择,只能绕过null
和undefined
。它们不可避免地会导致空引用异常,以及非常经典的undefined is not a function ……
那么,TypeScript 如何帮助我们解决这个问题?
TypeScript 和价值的缺失
因此,TypeScript 没有两种方式来表示值的缺失。它有四种。
表示价值缺失的四种方式是:
undefined
就像null
在 JavaScript 中一样,因为 TypeScript 毕竟是 JavaScript 的超集void
表示不返回任何内容的函数的返回类型never
表示永不返回的函数的返回类型(例如,它会引发异常)
哇!四个而不是两个??这有什么好处吗?答案是肯定的。不是因为表示值缺失的方式有很多,而是因为在 TypeScript 中null
和undefined
也是类型。
所以呢?
让我们看一下前面的例子。我们定义一个Target
接口来表示任何具有happiness
属性的事物(人类、宠物或怪物只要可以被拥抱就行):
interface Target {
happiness: number;
}
现在我们可以在我们的hug
函数中使用它了:
// Love!!!!!
function hug(target: Target) {
target.happiness += 1000;
}
如果我们像之前那样尝试拥抱悲伤的 Jaime,一切都会好起来的:
// Love!!!!!
function hug(target: Target) {
target.happiness += 1000;
}
const sadJaime = magicMap.get("Jaime"); // that's me of course
hug(sadJaime); // => Wihooo! ❤️
但是如果我们尝试拥抱埃莉诺会怎么样?
// 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'.
我们收到错误!我们收到一个编译时错误。也就是说,只要我们输入上面的代码,TypeScript 编译器就会告诉我们哪里出错了。因为该函数hug
需要 a Target
,而我们传入的undefined
是一个完全不同的类型,它不具备 的任何特性Target
,所以 TypeScript 编译器会介入并提供帮助。
这就是 TypeScript 如何利用null
和undefined
类型来防止你遇到空引用异常以及可怕的百万美元错误。是不是很棒?
严格空值检查
我们刚刚看到的 TypeScript 会阻止你使用 null 或 undefined 来自找麻烦,这种行为被称为严格空值检查。它是 TypeScript 编译器的一项功能,于 TypeScript 2.0 版本中引入,需要在 tsconfig 文件中通过 stricNullChecks 设置在启用它。通常,强烈建议你尽可能严格地配置 TypeScript。这样,你就能充分利用 TypeScript 的类型系统,并编写更安全、更易于维护的应用程序。
只是为了好玩。我们该如何重写上面的函数,让 fornull
或undefined
and 等价于 JavaScript 版本呢?一种方法如下:
// Love!!!!!
function hug(target: Target | undefined | null) {
target.happiness += 1000;
}
因此,我们明确地告诉 TypeScript,该函数的参数可以是Target
,undefined
或者null
通过使用类型联合。或者:
// Love!!!!!
function hug(target?: Target | null) {
target.happiness += 1000;
}
其中我们使用一个可选参数,使用target?
的简写符号Target | undefined
。
使用 Null 或 Undefined
在某些情况下,你仍然需要使用null
或undefined
。假设你正在使用一个用 JavaScript 实现的第三方库,它不太关心返回null
或undefined
。你可能有一位强大的战士,正要用他的剑砍一些土豆作为圣诞晚餐,所以你这样写:
const warrior = tavern.hireWarrior({goldCoins: 2});
if (warrior !== null
&& warrior !== undefined
&& warrior.sword !== null
&& warrior.sword !== undefined){
warrior.sword.slash();
}
您使用的战士雇佣 API 是用 JavaScript 编写的,您无法确定它是否返回战士,或者战士是否有剑,因此为了安全起见,您被迫编写一堆空/未定义的检查。
TypeScript(>=3.7)有一个替代的、更简洁的版本,可以替代上面的模式。可选链式运算符?.
可以让我们重写上面的例子:
warrior?.sword?.slash();
// The ? is often called the Elvis operator
// because this ?:-p
也就是说,只有当和 her既不为 undefined 也不为 null时,我们才会调用该slash
方法。在我看来,这是一个更好的选择。warrior
sword
解决在声名狼藉的酒馆雇佣战士的问题的另一种方法是,手头有一个默认值,以便在出现问题时使用。因此,为了确保上述削减能够发生,我们可以采用始终确保强大的战士确实存在的方法:
let warrior = tavern.hireWarrior({goldCoins: 2});
if (!warrior) {
warrior = new Warrior('Backup Plan Joe');
}
if (!warrior.sword) warrior.equip(new RustySword());
warrior.sword.slash();
TypeScript 还提供了检查这是否为空以及是否分配其他值模式的替代方法:空值合并运算符 ??
。
我们可以??
简化上面的例子:
let warrior = tavern.hireWarrior({goldCoins: 2})
?? new Warrior('Backup plan Joe');
if (!warrior.sword) warrior.equip(new RustySword());
warrior.sword.slash();
不错吧??.
and??
运算符使得解决 TypeScript 中值缺失的问题变得更容易。
可选链式调用和空值合并运算符已作为 ES2020 的一部分获得批准,现已成为 JavaScript 语言的官方特性。太棒了!
有没有更好的方法来模拟价值的缺失?
一种非常酷的建模缺失值的方法来自函数式编程中以Maybe
monad 的形式。在本系列的后续文章中,当我们深入探究泛型的奥秘时,我们将重新讨论这个主题,并了解 TypeScript 如何支持这些函数式编程模式。
总之
null
并undefined
可能对你的 JavaScript 程序造成严重破坏。它们可能会在运行时随意破坏程序,或者迫使你编写大量冗长的保护原因和防御性代码,这些代码可能会非常冗长,并掩盖你的核心业务逻辑。
在 TypeScript 中null
, 和undefined
也是类型。结合严格的空值检查,您可以在编译时保护程序免受空引用异常的影响。这样,一旦您在运行时犯了一个可能导致 bug 的错误,您就可以立即得到反馈并立即修复。
当需要使用null
TypeScript undefined
3.7 和 ES2020 时,有两个功能可以减轻您的困扰。可选链式调用和空值合并运算符可以在处理 null 和 undefined 时节省大量的输入。
今天就到这里。希望你喜欢这篇文章,并且学到了一些新东西。下次再见,保重,祝你拥有美好的一天。
-
在 JavaScript 中,空引用异常被视为更通用的 TypeError。与其他语言(例如 C# 或 Java)不同,JavaScript 中没有仅适用于空引用异常的特定错误(NullReferenceException) 。↩