继上篇文章TypeScript 学习:TypeScript for JavaScript Programmers之后,我们继续来看 TypeScript 文档的手册介绍与基础部分。
关于本手册
在 JavaScript 诞生的 20 多年来,它是有史以来最广泛使用的跨平台语言之一。 它最初是一种用于向网页添加琐碎交互的小型脚本语言,现已发展成为前端和后端应用程序的首选语言。虽然 JavaScript 编写的程序的大小与复杂性呈指数增长,但 JavaScript 语言却没有表达不同代码单元之间关系的能力。结合 JavaScript 相当奇特的运行时语义,语言和程序复杂性之间的这种不匹配使得 JavaScript 难以成为一项开发大规模任务的语言。
开发者编写代码的最常见错误莫过于类型错误:比如本来期望 number 类型的,但实际却是 string 类型,这可能是由于拼写错误、没有理解库的 API 接口、对运行时行为理解错误等等。 TypeScript 的目标是成为 JavaScript 程序的静态类型检查器,换句话说,是一种在代码运行之前运行(静态)确保程序类型正确(类型检查)的工具。
本手册的结构
该手册分为两部分:
手册
你可能期望每一章或每一页都能让你对给定的概念有深刻的理解。但 TypeScript 手册不是完整的语言规范,它旨在成为该语言所有特性和行为的综合指南。
阅读完本手册的读者能够:
- 阅读并理解常用的 TypeScript 语法和模式
- 解释重要编译器选项的作用
- 大多数情况下能正确预测类型系统的行为
为了清晰和简洁起见,本手册的主要内容不会探讨所涵盖功能的每个边缘案例或细节。您可以在参考文章中找到有关特定概念的更多详细信息。
参考文档
左侧边栏的参考部分(Reference)旨在提供对 TypeScript 特定部分如何工作的更丰富的理解。您可以从头到尾阅读它,但每个部分都旨在对单个概念进行深入的解释,所以这没有连续性。
本手册还旨在成为一份简洁的文档,可以在几个小时内轻松阅读。为了简短起见,将不涵盖某些主题。
具体来说,该手册并未完整介绍函数、类和闭包等核心 JavaScript 基础知识。我们将包含指向这些内容的背景阅读链接,您可以使用这些链接来阅读这些概念。
该手册也无意替代语言规范。在某些情况下,将跳过边缘案例或行为的详细描述,以支持更高级、更易于理解的解释。但是有单独的参考页面可以更准确、更正式地描述 TypeScript 的行为。参考页面适合熟悉 TypeScript 的读者,因为这里可能使用高级术语。
最后,本手册除非必要,否则不会涵盖 TypeScript 如何与其他工具交互。如: webpack、rollup、parcel、react、babel、closure、lerna、rush、bazel、preact、vue、angular、svelte、jquery、yarn 或 npm 配置等。您可以在网上找到这些资源。
TypeScript 基础
JavaScript 中的每个值都有一组行为,您可以通过运行不同的操作观察到这些行为。这听起来很抽象,但请看下面的例子,我们对名为 message 的变量执行一些操作。
第一行可运行代码访问名为 toLowerCase 的属性,然后调用它。第二行尝试直接调用 message 。
但假设我们不知道 message 的值是什么,所以就无法可靠地说出运行这段代码会得到什么结果。这些操作的结果完全取决于 message 的值:
message是可调用的吗?- 它有一个名为
toLowerCase的属性吗? - 如果有,
toLowerCase可以调用吗? - 如果这两个都是可调用的,它们会返回什么?
这些问题的答案通常是我们在编写 JavaScript 时记在脑子里的东西,我们必须保证所有细节都正确。
假设 message 如下定义:
如果我们运行 message.toLowerCase() ,会得到小写的字符串。
但执行 message() 呢?这会失败并抛出异常 TypeError 。
如果能避免这样的错误就太好了。
当执行代码时,JavaScript 运行时通过确定值的类型来决定它有什么样的行为和能力,这也是报错 TypeError 的原因,它说字符串 "Hello World!" 不能作为函数调用。
对于某些值,例如原始类型 string 和 number ,可以在运行时使用 typeof 运算符识别它们的类型。但是对于函数之类的内容,没有相应的运行时机制来识别类型。比如下面这个函数:
此代码表示在 x 有 flip 函数时可用,但 JavaScript 不会显示的展示出信息, JavaScript 中判断 fn 对 x 做了什么的唯一方法是调用它并查看会发生什么。这种行为使得很难预测代码在运行之前会做什么,这意味着在编写代码时更难知道代码将要做什么。
由此看来,类型可以描述哪些值可以传递给 fn 以及哪些值传递给 fn 会崩溃。 JavaScript 只提供了动态类型,即运行代码看看会发生什么。
另一种方法是使用静态类型系统在运行前预测代码的执行。
静态类型检查
回想一下之前尝试将 string 作为函数调用时得到的 TypeError 。我们不喜欢在运行代码时出现任何类型的错误,这可能是 bug,当我们编写代码时,会尽力避免引入错误。
如果我们只写一点代码,保存,重新运行,并会立即看到错误,我们可能能够快速找到问题,但事实并非总是如此,我们可能漏掉了报错情况的测试,导致可能出现潜在的风险。
理想情况下,最好有一个工具来帮助我们在代码运行之前找到这些错误。就像 TypeScript 这样的静态类型检查器所做的。静态类型系统描述了运行程序时期望变量的形状和行为。TypeScript 类型检查器使用如下信息并告诉我们什么时候变量可能会偏离预期。
使用 TypeScript 运行如上示例将首先在运行代码之前给出一条错误消息。
非异常失败
我们上面讨论的运行时错误,根据 ECMAScript 规范,会抛错。你也许认为这是“显而易见的行为”,你也可以想象访问对象上不存在的属性也应该抛错。但是,JavaScript 没有抛错而是返回值 undefined :
但作为静态类型系统 TypeScript 必须知道哪些代码应该标记为错误,即使它在 JavaScript 不会抛错。在 TypeScript 中,以下代码会产生有关 location 未定义的错误:
虽然有时这意味着在您必须在表达的内容与类型定义进行权衡,但其目的是在代码中捕获潜在的错误。
例如:错别字
未调用的函数
或基本逻辑错误(没必要使用 else if,直接写 else 即可)
工具类型
TypeScript 不仅可以在代码中出错时,捕获错误,还能一开始就防止我们犯这些错误。
类型检查器有能力来检查我们访问变量的属性。它还可以建议提示您可能想要使用到的属性。
TypeScript 非常重视工具,不仅是您写代码时的错误提示和自动完成。支持 TypeScript 的编辑器还可以提供“快速修复”以自动修复错误、重构以轻松的重新组织代码,和用于跳转到变量定义或查找对给定变量的所有引用的功能。所有这些都建立在类型检查器之上,并且是完全跨平台的。
tsc TypeScript 编译器
我们一直在谈论类型检查,但还没有使用类型检查器。让我们认识一下新朋友 tsc ,TypeScript 编译器。首先需要通过 npm 获取它。
npm install -g typescript
尝试编写第一个 TypeScript 程序: hello.ts :
// Greets the world.
console.log("Hello world!");
这里没有多余的代码;这个“hello world”程序看起来与用 JavaScript 编写的“hello world”程序完全相同。现在让我们通过运行 typescript 包为我们安装的命令 tsc 来对其进行类型检查。
tsc hello.ts
我们运行了 tsc ,因为没有类型错误,所以没有在控制台中输出,也没有什么可报告的。
但仔细一看,我们得到了一些文件输出,在 hello.ts 旁边有一个 hello.js 文件。这是 hello.ts 文件在 tsc 编译为纯 JavaScript 文件后的输出, JavaScript 文件内容如下:
// Greets the world.
console.log("Hello world!");
在这种情况下,TypeScript 几乎没有什么可以转换的,所以它看起来和我们写的一样。编译器试图编译出干净可读的代码。编译后的代码会帮助我们缩进,并且保留注释。
那当代码有类型错误时,会发生什么?重写 hello.ts 如下:
function greet(person, date) {
console.log(`Hello ${person}, today is ${date}!`);
}
greet('Brendan');
重新运行 tsc hello.ts,在命令行收到错误:
Expected 2 arguments, but got 1.
TypeScript 告诉我们忘记将参数传递给 greet 函数。目前为止,我们只编写了标准的 JavaScript,但 TypeScript 仍然能够发现我们代码的问题。
编译代码,即使携带错误
上个示例中你可能没有注意到即使 TypeScript 报错了,但 hello.js 文件仍然重新生成了。考虑到 tsc 报告了关于代码的错误,这可能有点令人惊讶,但这是基于 TypeScript 的核心思想之一:很多时候,你会比 TypeScript 更清楚你的代码。
类型检查代码限制了您可以运行的代码类型,因此需要权衡类型检查器可接受的类型。大多数情况下这没问题,但在某些情况下这些检查会妨碍您。例如,想象一下您将 JavaScript 代码迁移到 TypeScript 并引入了类型检查错误。当原来的 JavaScript 代码已经可以工作了,所以 TypeScript 没有必要阻止你去编译它。
所以 TypeScript 不会妨碍您。当然,随着时间的推移,您可能希望更能防范错误,并使 TypeScript 的行为更加严格。在这种情况下,您可以使用 noEmitOnError 编译器选项。
tsc --noEmitOnError hello.ts
这时 hello.js 就不会更新
显示声明类型
我们还没有告诉 TypeScript person 或 date 是什么类型。让我们编辑代码以告诉 TypeScript person 是一个 string ,而 date 应该是一个 Date 对象。我们还将在 date 上使用 toDateString() 方法。
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
如上使用类型注释解释了 person 是 string 类型, date 是 Date 对象
有了这个,TypeScript 可以提示我们 greet 可能被错误调用的情况。如:
嗯? TypeScript 在我们的第二个参数上报告了错误,但为什么呢?
也许令人惊讶的是,在 JavaScript 中调用 Date() 会返回 string 。另一方面,用 new Date() 会构造一个 Date ,实际上这是我们想要的。所以修复如下
请记住,我们不必总是编写显式类型注释。大多情况下,TypeScript 可以为我们推断类型。
灰色小框为鼠标悬停到变量上的提示
即使我们没有告诉 TypeScript msg 的类型是 string ,它也能弄清楚。这是一个特性,当类型系统可以自动推断出相同的类型时,最好不要显示添加类型注释。
消失的类型
让我们来看下 greet 被 tsc 编译成 JavaScript 的内容:
注意 2 件事:
- person 和 date 参数的类型注释消失了
- 我们写的模板字符串语法变成了 concat 调用
让我们先关注第一点。类型注解不是 JavaScript 的一部分,因此没有任何浏览器或其他运行时可以不加修改地运行 TypeScript 代码。这就是为什么 TypeScript 首先需要一个编译器(它需要某种方法来剥离或转换任何 TypeScript 特有的语法),以便您可以运行它。所有特定于 TypeScript 的代码都被删除了,如这里的类型注释也被完全删除了。
请注意:类型注释永远不会改变程序的运行时行为。
降级
上面的第二点,从模板字符串变成了 concat 方法
`Hello ${person}, today is ${date.toDateString()}!`
// 变成了
"Hello ".concat(person, ", today is ").concat(date, "!")
模板字符串是 ECMAScript 2015 版本的一项功能。 TypeScript 能够将代码从新版本的 ECMAScript 重写到旧版本,例如 ECMAScript 3 或 ECMAScript 5。从较新或“更高”版本的 ECMAScript 向下转换到较旧或“较低”版本的过程有时称为降级。
默认情况下,TypeScript 以 ES3 为目标,这是 ECMAScript 的一个非常旧的版本。我们可以使用 target 选项配置。使用 --target es2015 将 TypeScript 更改为目标 ECMAScript 2015,这意味着代码应该能够在支持 ECMAScript 2015 的任何地方运行。所以运行 tsc --target es2015 hello.ts 输出如下:
function greet(person, date) {
console.log(`Hello ${person}, today is ${date}!`);
}
虽然默认目标是 ES3,但当前大多数浏览器都支持 ES2015。因此,大多数开发人员将 ES2015 或更高版本指定为目标,除非你需要与某些旧版浏览器兼容。
严格性
不同的用户使用 TypeScript 在类型检查方面有不同的用法。有些人希望以一种宽松的配置加入,它帮助他们仅验证程序的某些部分,并且仍然拥有 TypeScript 不错的类型检查与提示。这是 TypeScript 的默认体验,其中配置是可选的,默认采用最宽松的配置,并且不检查潜在的 null / undefined 值。这些默认值已就绪,如果您要迁移现有的 JavaScript 代码,那这应该是你希望的。
但是更多用户喜欢让 TypeScript 尽可能多地校验,这就是该语言提供严格配置的原因。这些严格选项配置的越多,就会为你检查越多。这可能需要一些额外的工作,但总的来说,从长远看这是值得的,因为可以进行更彻底和更准确的检查。如果可能,新代码库应始终启用这些严格检查。
TypeScript 可通过 strict 标志来开启或关闭严格检查,其中你最好了解 2 个配置:
noImplicitAny
在某些情况下,TypeScript 不会尝试为我们推断类型,而是回退到最宽松的类型: any 。这不是最糟糕的事情,毕竟,回退到 any 只是普通的 JavaScript 体验而已。
但是,使用 any 通常会违背使用 TypeScript 的初衷。您的程序类型越多,您获得的验证和工具就越多,这意味着您在编写代码时遇到的错误会更少。打开 noImplicitAny 标志将对类型被隐式推断为 any 的任何变量发出错误。
strictNullChecks
默认情况下, null 和 undefined 之类的值可分配给任何其他类型。这可以使编写一些代码更容易,但是忘记处理 null 和 undefined 是世界上无数错误的原因! strictNullChecks 标志使处理 null 和 undefined 更加明确,并且让我们不必担心我们是否忘记处理 null 和 undefined 。