免责声明: 本文为翻译自 TypeScript 官方手册内容,非原创。版权归原作者所有。
原文来源: The Basics
翻译者注: 本人在翻译过程中已尽最大努力确保内容的准确性和完整性,但翻译工作难免有疏漏。如读者在阅读中发现任何问题,欢迎提出建议或直接查阅原文以获取最准确的信息。
JavaScript 中的每个值会随着我们执行不同的操作表现出一系列的行为。这听起来很抽象,看下面的例子,考虑一下针对变量 message 可能执行的操作。
// 访问 message 的 toLowerCase 方法并调用它
message.toLowerCase();
// 调用 message 函数
message();
我们来分解这个代码,第一行代码访问了一个名为 toLowerCase 的方法并调用它。第二行代码尝试直接调用 message 函数。
但是假设我们不知道 message 的值——这是相当常见的——我们无法可靠地说出尝试运行这段代码会得到什么结果。每个操作的行为完全取决于我们在一开始拥有的值。
message可以被直接调用吗?- 它是否具有名为
toLowerCase的属性? - 如果有,
toLowerCase是否可以被调用? - 如果
message和message.toLowerCase都可以被调用,它们会返回什么?
当我们编写 JavaScript 时,对于这些问题我们通常需要非常仔细,并且处理好所有细节。
假设 message 是以下方式定义的。
const message = "Hello World!";
正如你可能猜到的那样,如果我们尝试运行 message.toLowerCase(),我们将得到全部小写的字符串。
那如果运行 message() 呢?如果你熟悉 JavaScript,你会知道这会导致异常:
TypeError: message is not a function
如果我们能避免这样的错误就太好了。
当我们执行代码时,JavaScript 运行时会计算出值的类型 —— 这种类型有什么行为和功能,从而决定采取什么措施。TypeError 部分暗示了这一点 — 它表示字符串 "Hello World!" 不能被作为函数调用。
对于某些值,比如原始类型 string 和 number,我们可以使用 typeof 运算符在运行时识别它们的类型。但是对于其他类型,比如函数,没有相应的运行时机制来识别它们的类型。例如,考虑以下函数:
function fn(x) {
return x.flip();
}
我们通过阅读代码观察到,只有在给定一个具有可调用的 flip 属性的对象时,这个函数才能正常工作,但是 JavaScript 无法在代码运行时以一种我们可以检查的方式呈现这些信息。在纯 JavaScript 中,判断 fn 对特定值执行的操作的唯一方法是调用它并查看会发生什么。这种行为使得在代码运行之前很难预测它将要做什么,这意味着在编写代码时更难知道你的代码将要做什么。
从这个角度来看,类型就是描述哪些值可以传递给 fn 并且哪些值会导致崩溃的概念。JavaScript 只提供动态类型 - 运行代码以观察结果。
另一种选择是使用静态类型系统,在运行之前对代码进行预测。
静态类型检查
回想一下我们之前遇到的 TypeError,当我们试图将一个字符串作为函数调用时。大多数人在运行代码时不喜欢出现任何错误-这些被视为bug!而且当我们编写新代码时,我们尽力避免制造新的bug。
如果我们添加了一点代码,保存文件,重新运行代码,并立即看到错误,那么我们可能能够快速地找到问题所在;但情况并非总是如此。也许我们没有对该功能进行足够彻底的测试,因此实际上可能永远不会遇到潜在的错误!或者如果幸运地目睹了错误发生,那么可能已经进行了大规模重构并添加了很多不同的代码,这样就被迫深入分析以找到问题所在。
理想情况下,在我们的代码运行之前有一个帮助我们找出这些 bug 的工具。这就是像 TypeScript 这样的静态类型检查器所做的事情。静态类型系统描述了当程序运行时值将具有哪些结构和行为。像 TypeScript 这样的类型检查器使用该信息,并告诉我们什么时候事情可能会失控。
const message = "hello!";
message();
// This expression is not callable.
// Type 'String' has no call signatures.
使用 TypeScript 运行上面这个示例,它会在我们执行代码之前抛出错误。
非异常失败(Non-exception Failures)
到目前为止,我们一直在讨论运行时错误-当 JavaScript 运行时告诉我们它认为某些东西是错误的情况。这些情况出现是因为 ECMAScript 规范对语言在遇到意外情况时应该如何行为有明确的指示。
例如,规范说尝试调用一个不可调用的东西应该抛出一个错误。也许听起来像“显而易见的行为”,但你可以想象,在对象上访问一个不存在的属性也应该抛出一个错误。相反,JavaScript给了我们不同的行为,并返回值undefined:
const user = {
name: 'Daniel',
age: 26,
};
user.location; // 返回 undefined
最终,静态类型系统必须决定哪些代码应该被标记为错误,即使它是“有效”的 JavaScript 代码,不会立即抛出错误。在 TypeScript 中,以下代码会产生一个关于 location 未定义的错误:
const user = {
name: "Daniel",
age: 26,
};
user.location;
// Property 'location' does not exist on type '{ name: string; age: number; }'.
有时候这意味着你在表达上需要做出一些妥协,但目的是为了捕捉程序中的合法 bug。TypeScript 能够捕获很多合法 bug。
例如:拼写错误,
const announcement = "Hello World!";
// 你需要花多久才能注意到拼写错误?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();
// 实际上正确的拼写是这样的……
announcement.toLocaleLowerCase();
未调用的函数
function flipCoin() {
// 应该是 Math.random()
return Math.random < 0.5;
// Operator '<' cannot be applied to types '() => number' and 'number'.
}
或者是基本的逻辑错误:
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
// ...
} else if (value === "b") {
// This comparison appears to be unintentional
// because the types '"a"' and '"b"' have no overlap.
// 永远无法到达这个分支
}
类型工具
TypeScript 可以在我们编写代码时捕获 bug。这很棒,而且它也能够在一开始就防止我们的代码出现错误。
类型检查器具有检查诸如我们是否在变量和其他属性上访问正确属性的信息。一旦它拥有了这些信息,它还可以开始建议您可能想要使用的属性。
这意味着 TypeScript 也可以用于编辑代码,并且核心类型检查器可以在您输入编辑器时提供错误消息和代码补全功能。人们经常会谈到 TypeScript 在工具层面的作用,这就是一个典型的例子。
TypeScript 非常重视工具,这不仅限于在您输入时提供代码补全和错误修复。支持 TypeScript 的编辑器可以提供“快速修复”功能,以自动修复错误、进行代码重构,并提供有用的导航功能,可跳转到变量的定义或查找给定变量的所有引用。所有这些都是基于类型检查器构建的,并且完全跨平台,因此你最喜欢的编辑器很可能也支持了 TypeScript。
支持 TypeScript 的编辑器可以通过“快速修复”功能自动修复错误,重构产生易组织的代码。同时,它还具备有效的导航功能,能够让我们跳转到某个变量定义的地方,或者找到对于给定变量的所有引用。
tsc,TypeScript编译器
我们一直在谈论类型检查,但是我们还没有使用过类型检查器。让我们来认识一下我们的新朋友 tsc,即 TypeScript编译器。首先,我们需要通过 npm 获取它。
npm install -g typescript
这将全局安装 TypeScript 编译器 tsc。如果您更喜欢从本地的 node_modules 包中运行 tsc,可以使用 npx 或类似工具。
现在让我们创建一个新文件夹,尝试编写我们的第一个 TypeScript 程序:hello.ts:
console.log('Hello world!');
请注意这里没有任何多余的修饰;这个 “hello world” 程序看起来与您在 JavaScript 中编写的 “hello world” 程序完全相同。现在,让我们通过运行 tsc 命令对其进行类型检查,该命令是由 typescript 软件包为我们安装的。
tsc hello.ts
呔!
等等,“呔”什么呢?我们运行了 tsc,却没有发生任何事情!好吧,由于没有类型错误,所以在控制台上没有输出,因为没有需要报告的内容。
但再次检查 - 我们得到了一些文件输出。如果我们查看当前目录,我们会看到一个 hello.js 文件和 hello.ts 文件并列。那就是在 tsc 将 hello.ts 编译或转换成纯 JavaScript 文件后的输出结果。如果我们检查其内容,就能看到 TypeScript 处理 .ts 文件后产生的结果:
console.log("Hello world!");
在这种情况下,TypeScript 需要转换的内容很少,所以它看起来与我们编写的代码完全相同。编译器试图生成干净易读的代码,使它看起来像是人编写的。尽管这并不总是那么容易,但 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!
报错时仍产出文件
有一件事你可能没有注意到,在一个例子中,hello.js 文件再次发生了变化。如果我们打开这个文件,会看到内容基本上与输入文件相同。鉴于 tsc 报告了关于代码的错误,这可能有点令人惊讶,但这是基于 TypeScript 的核心价值观之一:大部分时间里,你比 TypeScript 更清楚。
再次重申,对代码进行类型检查,会限制可以运行的程序的种类,因此类型检查器会进行权衡,以确定哪些代码是可以被接受的。大多数情况下都没问题,但也有一些场景会受到这些检查的干扰。例如,在将 JavaScript 代码迁移到 TypeScript 并引入类型检查错误时,请想象一下自己最终会为类型检查器整理好代码。但原始 JavaScript 代码已经能正常工作!为什么将其转换成 TypeScript 就不能继续运行呢?
因此 TypeScript 不会妨碍你进行操作。当然,随着时间推移,你可能希望更加防范错误,并使 TypeScript 表现得更加严格一些。在这种情况下,你可以使用 noEmitOnError 编译选项。尝试修改你的 hello.ts 文件并使用该标志来运行 tsc:
tsc --noEmitOnError hello.ts
你会注意到 hello.js 没有再发生变动了。
显式类型
到目前为止,我们还没有告诉 TypeScript person 和 date 是什么。让我们编辑代码,告诉 TypeScript person 是一个字符串,并且 date 应该是一个日期对象。我们还将在 date 上使用 toDateString() 方法。
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
我们所做的是在 person 和 date 上添加类型注释,以描述 greet 可以接受哪些类型的值。您可以将该签名理解为 greet 接受一个类型为 string 的 person 和一个类型为 Date 的 date”。
有了这个,TypeScript 可以告诉我们其他可能错误调用 greet 的情况。例如...
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", Date());
// Argument of type 'string' is not assignable to parameter of type 'Date'.
嗯?TypeScript在我们的第二个参数上报了一个错误,但为什么?
也许出乎意料的是,在 JavaScript 中调用 Date() 会返回一个 string。另一方面,使用 new Date() 构造一个 Date 实例实际上给了我们期望的结果。
无论如何,我们可以快速修复这个错误:
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", new Date());
记住,我们并不总是需要编写显式的类型注释。在许多情况下,即使我们省略了它们,TypeScript 甚至可以自动推断(或“找出”)类型。
尽管我们没有告诉 TypeScript msg 的类型是 string,但它能够推断出来。这是它的一个功能,并且最好在类型系统能推断相同类型时不添加注释。
注意:前面代码示例中的消息气泡是您的编辑器在悬停在单词上时显示的内容。
擦除类型
让我们来看一下使用 tsc 编译上述函数 greet 并输出 JavaScript 时会发生什么:
"use strict";
function greet(person, date) {
console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
greet("Maddison", new Date());
注意这里有两个变化:
- 我们的
person和date参数不再具有类型注释。 - 我们的“模板字符串”被转换为带有连接符的普通字符串。
稍后会详细讨论第二点,但现在让我们专注于第一点。类型注释并不是 JavaScript(或者严谨地说是 ECMAScript)的一部分,所以实际上没有任何浏览器或其他运行时可以直接运行未经修改的 TypeScript。这就是为什么 TypeScript 首先需要一个编译器 - 它需要某种方式来剥离或转换任何特定于 TypeScript 的代码,以便您可以运行它。大多数特定于 TypeScript 的代码都被抹掉了,在这里,我们的类型注释也完全被抹去了。
记住:类型注释不会改变程序的运行行为。
降级
上面的另一个变化,就是我们的模板字符串从:
`Hello ${person}, today is ${date.toDateString()}!`;
被重写为:
"Hello " + person + ", today is " + date.toDateString() + "!";
为什么会发生这种情况?
模板字符串是 ECMAScript 的一个特性,被称为 ECMAScript 2015(也叫做 ECMAScript 6、ES2015、ES6 等等)。TypeScript 可以将代码从较新的 ECMAScript 版本重写为较旧的版本,例如 ECMAScript 3 或者 ECMAScript 5(也叫做 ES3 和 ES5)。这个过程有时被称为降级,即从较新或“更高”的版本向下移动到较旧或“更低”的版本。
默认情况下,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.toDateString()}!`);
}
greet("Maddison", new Date());
虽然默认目标是ES3,但绝大多数现代浏览器都支持ES2015。因此,大多数开发者可以安全地将目标指定为ES2015或更高版本,除非需要与某些古老的浏览器兼容。
严格性
不同的用户使用 TypeScript 时对类型检查器有不同的期望。有些人希望获得一种更宽松的选择体验,可以验证程序中的某些部分,并且具备良好的工具支持。这是 TypeScript 的默认体验,其中类型是可选的,推断会采用最宽松的类型,并且不会检查潜在的 null/undefined 值。就像 tsc 在错误面前仍然能够生成输出一样,这些默认设置旨在避免干扰你。如果你正在迁移现有 JavaScript 代码,这可能是一个理想的第一步。
相比之下,很多用户更喜欢让 TypeScript 尽可能地进行验证,并且语言提供了严格性设置来满足这个需求。这些严格性设置将静态类型检查从开关(要么检查你的代码,要么不检查)转变为更接近于调节钮式样式。当你把该调节钮向上拧动时,TypeScript 将为你进行更多检查。虽然可能需要额外努力,但总体而言,在长期运行中它会回报自己,并提供更全面和准确的工具支持。新建项目时应始终打开这些严格性检查。
TypeScript 有几个可以打开或关闭的类型检查严格标志,默认情况下我们的示例将使用所有标志。在 CLI 中,strict 标志或 tsconfig.json 文件中的 "strict": true 可以同时打开它们,但我们也可以单独选择关闭某些标志。你应该了解的两个最重要的标志是 noImplicitAny 和 strictNullChecks。
noImplicitAny
回想一下,在某些地方,TypeScript 不会尝试为我们推断类型,而是退而求其次选择最宽松的类型:any。这并不是最糟糕的事情-毕竟,退回到 any 只是普通的 JavaScript 体验。
然而,使用 any 经常会削弱使用 TypeScript 的目的。程序越具有类型化,您将获得更多验证和工具支持,这意味着在编码过程中遇到的错误将更少。打开 ++noImplicitAny++ 标志将对任何隐式推断为 any 类型的变量发出错误提示。
strictNullChecks
默认情况下,null 和 undefined 这样的值可以赋给任何其他类型。这可能使编写某些代码更容易,但忘记处理 null 和 undefined 是世界上无数错误的原因 - 有人认为这是一个价值十亿美元的错误!++strictNullChecks++ 标志使处理 null 和 undefined 更加明确,并避免了我们担心是否忘记处理 null 和 undefined。