JavaScript 中的每个值都有一组行为,你可以通过运行不同的操作来观察这些行为。这听起来很抽象,但举个简单的例子,考虑一下我们可能对一个名为 message
的变量执行的一些操作。
js
// Accessing the property 'toLowerCase'// on 'message' and then calling itmessage.toLowerCase();// Calling 'message'message();
如果我们把它分解,第一行可运行代码访问了一个名为 toLowerCase
的属性,然后调用它。第二行尝试直接调用 message
。
但是,假设我们不知道 message
的值——这很常见——我们无法可靠地说运行这些代码会得到什么结果。每个操作的行为完全取决于我们最初的值。
message
可调用吗?- 它是否有名为
toLowerCase
的属性? - 如果有,
toLowerCase
可调用吗? - 如果这两个值都可调用,它们返回什么?
这些问题的答案通常是我们编写 JavaScript 时脑海中的东西,我们必须希望自己没有遗漏任何细节。
假设 message
是以下面的方式定义的。
js
const message = "Hello World!";
正如你可能猜到的,如果我们尝试运行 message.toLowerCase()
,我们将得到相同的字符串,只是小写。
第二行代码呢?如果你熟悉 JavaScript,你会知道这会导致异常
txt
TypeError: message is not a function
如果我们能避免这种错误,那就太好了。
当我们运行代码时,JavaScript 运行时选择执行操作的方式是通过确定值的类型——它具有哪些行为和功能。这就是 TypeError
所指的一部分——它表示字符串 "Hello World!"
不能作为函数调用。
对于某些值,例如 string
和 number
原语,我们可以使用 typeof
运算符在运行时识别它们的类型。但是对于其他东西,例如函数,没有相应的运行时机制来识别它们的类型。例如,考虑以下函数
js
function fn(x) {return x.flip();}
我们可以通过阅读代码观察到,该函数只有在给定一个具有可调用 flip
属性的对象时才会起作用,但 JavaScript 不会以一种可以在代码运行时检查的方式提供此信息。在纯 JavaScript 中,唯一能够判断 fn
对特定值执行的操作的方法是调用它并查看发生了什么。这种行为使得在代码运行之前很难预测代码会做什么,这意味着在编写代码时更难知道代码会做什么。
从这个角度来看,类型是描述哪些值可以传递给 fn
以及哪些值会导致崩溃的概念。JavaScript 实际上只提供动态类型——运行代码以查看发生了什么。
另一种方法是使用静态类型系统来在代码运行之前预测代码预期执行的操作。
静态类型检查
回想一下我们之前尝试将string
作为函数调用时得到的TypeError
。大多数人都不喜欢在运行代码时遇到任何类型的错误——这些都被认为是错误!当我们编写新代码时,我们会尽力避免引入新的错误。
如果我们添加一些代码,保存文件,重新运行代码,并立即看到错误,我们可能能够快速隔离问题;但这并不总是这样。我们可能没有对功能进行充分的测试,因此我们可能永远不会遇到可能抛出的潜在错误!或者,如果我们有幸目睹了错误,我们最终可能进行了大量的重构并添加了许多不同的代码,我们被迫从中挖掘。
理想情况下,我们可以拥有一个工具来帮助我们在代码运行之前找到这些错误。这就是像 TypeScript 这样的静态类型检查器所做的。静态类型系统描述了我们在运行程序时值的形状和行为。像 TypeScript 这样的类型检查器使用这些信息,并在事情可能偏离轨道时告诉我们。
tsTry
constmessage = "hello!";This expression is not callable. Type 'String' has no call signatures.2349This expression is not callable. Type 'String' has no call signatures.(); message
使用 TypeScript 运行最后一个示例将在我们运行代码之前给我们一个错误消息。
非异常故障
到目前为止,我们一直在讨论一些事情,比如运行时错误——在这些情况下,JavaScript 运行时会告诉我们它认为某些东西是荒谬的。这些情况出现是因为 ECMAScript 规范 对语言在遇到意外情况时应该如何表现有明确的说明。
例如,规范指出尝试调用不可调用对象应该抛出错误。这听起来可能像是“显而易见的行为”,但你可以想象访问对象上不存在的属性也应该抛出错误。相反,JavaScript 给我们提供了不同的行为,并返回了值 undefined
js
const user = {name: "Daniel",age: 26,};user.location; // returns undefined
最终,静态类型系统必须决定哪些代码应该在其系统中被标记为错误,即使它是“有效的”JavaScript,不会立即抛出错误。在 TypeScript 中,以下代码会产生关于 location
未定义的错误
tsTry
constuser = {name : "Daniel",age : 26,};Property 'location' does not exist on type '{ name: string; age: number; }'.2339Property 'location' does not exist on type '{ name: string; age: number; }'.user .; location
虽然有时这意味着我们在表达能力上的权衡,但其目的是在我们的程序中捕获真正的错误。而 TypeScript 捕获了大量的真正错误。
例如:拼写错误,
tsTry
constannouncement = "Hello World!";// How quickly can you spot the typos?announcement .toLocaleLowercase ();announcement .toLocalLowerCase ();// We probably meant to write this...announcement .toLocaleLowerCase ();
未调用的函数,
tsTry
functionflipCoin () {// Meant to be Math.random()returnOperator '<' cannot be applied to types '() => number' and 'number'.2365Operator '<' cannot be applied to types '() => number' and 'number'.Math .random < 0.5;}
或基本的逻辑错误。
tsTry
constvalue =Math .random () < 0.5 ? "a" : "b";if (value !== "a") {// ...} else if (This comparison appears to be unintentional because the types '"a"' and '"b"' have no overlap.2367This comparison appears to be unintentional because the types '"a"' and '"b"' have no overlap.value === "b") {// Oops, unreachable}
类型用于工具
TypeScript 可以捕获我们在代码中犯错时出现的错误。这很棒,但 TypeScript 还可以防止我们从一开始就犯这些错误。
类型检查器拥有信息来检查我们是否正在访问变量和其他属性的正确属性。一旦它拥有这些信息,它就可以开始建议你可能想要使用的属性。
这意味着 TypeScript 可以用于编辑代码,并且核心类型检查器可以在你编辑器中输入时提供错误消息和代码补全。这正是人们在谈论 TypeScript 中的工具时经常提到的部分。
tsTry
importexpress from "express";constapp =express ();app .get ("/", function (req ,res ) {res .sen });app .listen (3000);
TypeScript 非常重视工具,这不仅仅体现在你输入时的补全和错误上。支持 TypeScript 的编辑器可以提供“快速修复”来自动修复错误,重构以轻松地重新组织代码,以及有用的导航功能,用于跳转到变量的定义,或查找给定变量的所有引用。所有这些都建立在类型检查器的基础之上,并且完全跨平台,因此你最喜欢的编辑器很可能 已经支持 TypeScript.
tsc
,TypeScript 编译器
我们一直在讨论类型检查,但还没有使用我们的类型检查器。让我们认识一下我们的新朋友 `tsc`,也就是 TypeScript 编译器。首先,我们需要通过 npm 获取它。
sh
npm install -g typescript
这会在全局安装 TypeScript 编译器 `tsc`。如果你更喜欢从本地 `node_modules` 包运行 `tsc`,可以使用 `npx` 或类似工具。
现在让我们移动到一个空文件夹,尝试编写我们的第一个 TypeScript 程序:`hello.ts`
tsTry
// Greets the world.console .log ("Hello world!");
注意这里没有花哨的东西;这个“hello world”程序看起来与你在 JavaScript 中编写的“hello world”程序完全相同。现在让我们通过运行命令 `tsc` 来进行类型检查,该命令由 `typescript` 包为我们安装。
sh
tsc hello.ts
太棒了!
等等,“太棒了”到底是什么意思?我们运行了 `tsc`,但什么也没发生!好吧,没有类型错误,所以我们没有在控制台中得到任何输出,因为没有东西要报告。
但再检查一下 - 我们得到了一些文件输出。如果我们在当前目录中查看,我们会看到一个 `hello.js` 文件,它位于 `hello.ts` 旁边。这是 `tsc` 将 `hello.ts` 文件编译或转换为普通 JavaScript 文件后的输出。如果我们检查内容,我们会看到 TypeScript 在处理 `.ts` 文件后吐出的内容。
js
// Greets the world.console.log("Hello world!");
在这种情况下,TypeScript 要转换的东西很少,所以它看起来与我们编写的内容相同。编译器试图发出干净易读的代码,看起来像是人写的。虽然这并不总是那么容易,但 TypeScript 会一致地缩进,会注意我们的代码何时跨越不同的代码行,并会尝试保留注释。
如果我们确实引入了类型检查错误会怎样?让我们重写 `hello.ts`
tsTry
// This is an industrial-grade general-purpose greeter function:functiongreet (person ,date ) {console .log (`Hello ${person }, today is ${date }!`);}greet ("Brendan");
如果我们再次运行 `tsc hello.ts`,请注意我们在命令行上得到一个错误!
txt
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
sh
tsc --noEmitOnError hello.ts
您会注意到 hello.js
从未更新。
显式类型
到目前为止,我们还没有告诉 TypeScript person
或 date
是什么。让我们编辑代码,告诉 TypeScript person
是一个 string
,并且 date
应该是一个 Date
对象。我们还将在 date
上使用 toDateString()
方法。
tsTry
functiongreet (person : string,date :Date ) {console .log (`Hello ${person }, today is ${date .toDateString ()}!`);}
我们所做的是在 person
和 date
上添加了类型注解,以描述可以调用 greet
的值类型。您可以将该签名理解为“greet
接受一个类型为 string
的 person
和一个类型为 Date
的 date
”。
有了这个,TypeScript 可以告诉我们其他可能错误调用 greet
的情况。例如…
tsTry
functiongreet (person : string,date :Date ) {console .log (`Hello ${person }, today is ${date .toDateString ()}!`);}Argument of type 'string' is not assignable to parameter of type 'Date'.2345Argument of type 'string' is not assignable to parameter of type 'Date'.greet ("Maddison",Date ());
嗯?TypeScript 在我们的第二个参数上报告了一个错误,但为什么呢?
可能令人惊讶的是,在 JavaScript 中调用 Date()
会返回一个 string
。另一方面,使用 new Date()
构造一个 Date
实际上给了我们我们期望的东西。
无论如何,我们可以快速修复这个错误
tsTry
functiongreet (person : string,date :Date ) {console .log (`Hello ${person }, today is ${date .toDateString ()}!`);}greet ("Maddison", newDate ());
请记住,我们并不总是需要编写显式类型注解。在很多情况下,即使我们省略了它们,TypeScript 也可以推断(或“推断出”)类型。
tsTry
letmsg = "hello there!";
即使我们没有告诉 TypeScript msg
的类型是 string
,它也能推断出来。这是一个特性,如果类型系统最终会推断出相同的类型,最好不要添加注解。
注意:之前代码示例中的消息气泡是你的编辑器在你将鼠标悬停在该词上时会显示的内容。
擦除类型
让我们看看当我们使用 tsc
编译上面的函数 greet
以输出 JavaScript 时会发生什么
tsTry
"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 特定的代码会被删除,同样,这里的类型注解也被完全删除了。
请记住:类型注解永远不会改变程序的运行时行为。
降级
与上面不同的是,我们的模板字符串被重写了,从
js
`Hello ${person}, today is ${date.toDateString()}!`;
到
js
"Hello ".concat(person, ", today is ").concat(date.toDateString(), "!");
为什么会发生这种情况?
模板字符串是 ECMAScript 的一个版本(称为 ECMAScript 2015,也称为 ECMAScript 6、ES2015、ES6 等——别问)中的一个特性。TypeScript 能够将来自 ECMAScript 的较新版本(如 ECMAScript 3 或 ECMAScript 5)的代码重写为较旧的版本(也称为 ES3 和 ES5)。这种从 ECMAScript 的较新版本(或“更高”版本)向下迁移到较旧版本(或“更低”版本)的过程有时被称为降级。
默认情况下,TypeScript 针对 ES3,这是一个非常旧的 ECMAScript 版本。我们可以使用 target
选项选择一个稍微更新一点的版本。使用 --target es2015
运行会将 TypeScript 的目标更改为 ECMAScript 2015,这意味着代码应该能够在任何支持 ECMAScript 2015 的地方运行。因此,运行 tsc --target es2015 hello.ts
会得到以下输出
js
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
。