# 基础知识

JavaScript 中的每个值都有一组行为,您可以通过运行不同的操作来观察这些行为。这听起来很抽象,但作为一个简单的例子,考虑我们可能在名为 message的变量上运行的一些操作。

// Accessing the property 'toLowerCase'
// on 'message' and then calling it
message.toLowerCase();

// Calling 'message'
message();

如果我们将其分解,第一行可运行的代码会访问一个名为 toLowerCase的属性,然后调用它。第二个尝试直接调用 message

但是假设我们不知道 message的值——这很常见——我们不能可靠地说明尝试运行这些代码会得到什么结果。每个操作的行为完全取决于我们最初拥有的价值。

  • message可以调用吗?
  • 它上面是否有一个名为 toLowerCase的属性?
  • 如果是这样,toLowerCase甚至可以调用吗?
  • 如果这两个值都是可调用的,它们会返回什么?

这些问题的答案通常是我们在编写 JavaScript 时牢记在心的事情,我们必须希望我们得到了正确的所有细节。

假设 message是按以下方式定义的。

const message = "Hello World!";

正如您可能猜到的,如果我们尝试运行 message.toLowerCase(),我们将只得到相同的小写字符串。

那第二行代码呢?如果您熟悉 JavaScript,您会知道这会失败并出现异常:

TypeError: message is not a function

如果我们能避免这样的错误,那就太好了。

当我们运行我们的代码时,我们的 JavaScript 运行时选择做什么的方式是确定值的类型——它具有什么样的行为和能力。这就是 TypeError所暗示的部分内容——它表示字符串 "Hello World!"不能作为函数调用。

对于某些值,例如原语 stringnumber,我们可以在运行时使用 typeof运算符识别它们的类型。但是对于其他的东西,比如函数,没有相应的运行时机制来识别它们的类型。例如,考虑这个函数:

function fn(x) {
  return x.flip();
}

我们可以通过阅读代码观察到,这个函数只有在给定一个具有可调用 flip属性的对象时才能工作,但 JavaScript 不会以我们可以在代码运行时检查的方式显示这些信息。在纯 JavaScript 中,判断 fn对特定值做了什么的唯一方法是调用它并查看会发生什么。这种行为使得在运行之前很难预测代码会做什么,这意味着在编写代码时更难知道代码会做什么。

这样看,类型就是描述哪些值可以传递给fn,哪些会崩溃的概念。JavaScript 仅真正提供动态类型 - 运行代码以查看发生了什么。

另一种方法是使用静态类型系统在运行之前预测预期的代码。

# 静态类型检查

回想一下我们之前尝试将 string作为函数调用而得到的 TypeError。大多数人不喜欢在运行他们的代码时遇到任何类型的错误——那些被认为是错误!当我们编写新代码时,我们会尽力避免引入新的错误。

如果我们只添加一点代码,保存我们的文件,重新运行代码,然后立即看到错误,我们也许可以快速隔离问题;但情况并非总是如此。我们可能没有对这个功能进行足够彻底的测试,所以我们可能永远不会真正遇到可能抛出的潜在错误!或者,如果我们有幸目睹了这个错误,我们可能最终会进行大规模的重构并添加许多我们不得不挖掘的不同代码。

理想情况下,我们可以有一个工具来帮助我们在代码运行之前找到这些错误。这就是像 TypeScript 这样的静态类型检查器所做的。静态类型系统描述了当我们运行程序时我们的值的形状和行为。像 TypeScript 这样的类型检查器使用这些信息并告诉我们什么时候事情可能会出轨。

const message = "hello!";

message();

在我们首先运行代码之前,使用 TypeScript 运行最后一个示例会给我们一个错误消息。

# 非异常故障

到目前为止,我们一直在讨论某些事情,比如运行时错误——JavaScript 运行时告诉我们它认为某些事情是荒谬的情况。出现这些情况是因为 ECMAScript 规范 明确说明了语言在遇到意外情况时应该如何表现。

例如,规范说尝试调用不可调用的东西应该会引发错误。也许这听起来像 "obvious behavior",但您可以想象访问对象上不存在的属性也会引发错误。相反,JavaScript 为我们提供了不同的行为并返回值 undefined

const user = {
  name: "Daniel",
  age: 26,
};

user.location; // returns undefined

最终,静态类型系统必须调用其系统中应将哪些代码标记为错误,即使是不会立即抛出错误的 "valid" JavaScript。在 TypeScript 中,以下代码会产生关于 location未定义的错误:

const user = {
  name: "Daniel",
  age: 26,
};

user.location;

虽然有时这意味着在您可以表达的内容上进行权衡,但其目的是捕捉我们程序中的合法错误。TypeScript 捕获了很多合法的错误。

例如:错别字,

const announcement = "Hello World!";

// How quickly can you spot the typos?
announcement.toLocaleLowercase();
announcement.toLocalLowerCase();

// We probably meant to write this...
announcement.toLocaleLowerCase();

未调用的函数,

function flipCoin() {
  // Meant to be Math.random()
  return Math.random < 0.5;
}

或基本逻辑错误。

const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
  // ...
} else if (value === "b") {
  // Oops, unreachable
}

# 工具类型

当我们在代码中出错时,TypeScript 可以捕获错误。这很好,但 TypeScript 也可以从一开始就阻止我们犯这些错误。

类型检查器具有检查诸如我们是否正在访问变量和其他属性的正确属性之类的信息。一旦有了这些信息,它还可以开始建议您可能想要使用哪些属性。

这意味着 TypeScript 也可以用于编辑代码,核心类型检查器可以在您在编辑器中键入时提供错误消息和代码完成。这是人们在谈论 TypeScript 工具时经常提到的部分内容。

import express from "express";
const app = express();

app.get("/", function (req, res) {
  res.sen
//       ^|
});

app.listen(3000);

TypeScript 非常重视工具,这超出了您键入时的完成和错误。支持 TypeScript 的编辑器可以提供 "quick fixes" 以自动修复错误、重构以轻松重新组织代码,以及用于跳转到变量定义或查找对给定变量的所有引用的有用导航功能。所有这些都建立在类型检查器之上,并且是完全跨平台的,所以很可能是 你最喜欢的编辑器支持 TypeScript

# tsc,TypeScript 编译器

我们一直在谈论类型检查,但我们还没有使用我们的类型检查器。让我们熟悉一下我们的新朋友 tsc,TypeScript 编译器。首先,我们需要通过 npm 获取它。

npm install -g typescript

这将全局安装 TypeScript Compiler tsc。如果您希望从本地 node_modules包运行 tsc,则可以使用 npx或类似工具。

现在让我们移动到一个空文件夹并尝试编写我们的第一个 TypeScript 程序:hello.ts

// Greets the world.
console.log("Hello world!");

请注意,这里没有多余的装饰;这个 "hello world" 程序看起来和你用 JavaScript 编写的 "hello world" 程序一样。现在让我们通过运行 typescript包为我们安装的命令 tsc来检查它。


tsc hello.ts

Tada!

等等,到底 "tada" 什么?我们跑了 tsc,什么也没发生!好吧,没有类型错误,所以我们没有在控制台中得到任何输出,因为没有什么要报告的。

但再次检查 - 我们得到了一些文件输出。如果我们查看当前目录,我们会在 hello.ts旁边看到一个 hello.js文件。这是 tsc编译或转换为纯 JavaScript 文件后我们的 hello.ts文件的输出。如果我们检查内容,我们会看到 TypeScript 在处理 .ts文件后会吐出什么:

// Greets the world.
console.log("Hello world!");

在这种情况下,TypeScript 几乎不需要转换,所以它看起来和我们写的一样。编译器试图发出看起来像人会写的东西的干净可读的代码。虽然这并不总是那么容易,但 TypeScript 会始终如一地缩进,注意我们的代码何时跨越不同的代码行,并试图保留注释。

如果我们确实引入了类型检查错误怎么办?让我们重写hello.ts

// This is an industrial-grade general-purpose greeter function:
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 persondate是什么。让我们编辑代码来告诉 TypeScript person是一个 string,而 date应该是一个 Date对象。我们还将在 date上使用 toDateString()方法。

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

我们所做的是在 persondate上添加类型注释来描述可以使用哪些类型的值来调用 greet。您可以将该签名读作 "greettakes a personof type string, and a dateof type Date"。

有了这个,TypeScript 可以告诉我们 greet可能被错误调用的其他情况。例如...

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

greet("Maddison", 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 甚至可以为我们推断(或 "figure out")类型,即使我们省略它们。


let msg = "hello there!";

即使我们没有告诉 TypeScript msgstring类型,它也能够弄清楚这一点。这是一个特性,当类型系统最终会推断出相同的类型时,最好不要添加注释。

注意:如果您将鼠标悬停在该单词上,则上一个代码示例中的消息气泡是您的编辑器将显示的内容。

# 擦除的类型

让我们看看当我们用 tsc编译上面的函数 greet以输出 JavaScript 时会发生什么:

function greet(person: string, date: Date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}

greet("Maddison", new Date());

这里注意两点:

  • 我们的 persondate参数不再有类型注释。
  • 我们的 "template string" - 那个使用反引号(```字符)的字符串 - 被转换为带有连接的纯字符串。

稍后会详细介绍第二点,但现在让我们关注第一点。类型注释不是 JavaScript 的一部分(或者 ECMAScript 是迂腐的),所以实际上没有任何浏览器或其他运行时可以在未经修改的情况下运行 TypeScript。这就是 TypeScript 首先需要一个编译器的原因——它需要某种方式来剥离或转换任何 TypeScript 特定的代码,以便您可以运行它。大多数特定于 TypeScript 的代码都被删除了,同样地,我们的类型注释也被完全删除了。

记住:类型注释永远不会改变程序的运行时行为。

# 降级

与上面的另一个区别是我们的模板字符串是从


`Hello ${person}, today is ${date.toDateString()}!`;

to


"Hello " + person + ", today is " + date.toDateString() + "!";

为什么会这样?

模板字符串是 ECMAScript 版本中的一项功能,称为 ECMAScript 2015(又名 ECMAScript 6、ES2015、ES6 等 - 不要问)。TypeScript 能够将代码从较新版本的 ECMAScript 重写为较旧的版本,例如 ECMAScript 3 或 ECMAScript 5(又名 ES3 和 ES5)。从 ECMAScript 的新版本或 "higher" 版本向下移动到旧版本或 "lower" 版本的过程有时称为降级。

默认情况下,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会同时将它们全部打开,但我们可以单独选择退出它们。您应该知道的两个最大的是 noImplicitAnystrictNullChecks

# noImplicitAny

回想一下,在某些地方,TypeScript 不会尝试为我们推断类型,而是回退到最宽松的类型:any。这并不是可能发生的最糟糕的事情——毕竟,回退到 any只是简单的 JavaScript 体验。

然而,使用 any通常会破坏使用 TypeScript 的初衷。您的程序类型越多,您获得的验证和工具就越多,这意味着您在编写代码时遇到的错误就越少。打开 noImplicitAny 标志将对任何类型隐式推断为 any的变量触发错误。

# strictNullChecks

默认情况下,像 nullundefined这样的值可以分配给任何其他类型。这可以使编写一些代码更容易,但忘记处理 nullundefined是世界上无数错误的原因 - 有些人认为它是 十亿美元的错误 !strictNullChecks 标志使处理 nullundefined更加明确,让我们不必担心是否忘记处理 nullundefined

Last Updated: 5/25/2023, 2:35:11 PM