Skip to content

基本

为社么需要使用TypeScript?

TypeScript
message.toLowerCase();
message();
// 如果我们拆分这个过程,那么第一行代码就是访问了 message 的 toLowerCase 方法并调用它。
// 第二行代码则尝试直接调用 message 函数。
message.toLowerCase();
message();
// 如果我们拆分这个过程,那么第一行代码就是访问了 message 的 toLowerCase 方法并调用它。
// 第二行代码则尝试直接调用 message 函数。

不过让我们假设一下,我们并不知道 message 的值 —— 这是很常见的一种情况,仅从上面的代码中我们无法确切得知最终的结果。 每个操作的结果完全取决于 message 的初始值。

  1. message 是否可以调用?
  2. 它有 toLowerCase 属性吗?
  3. 如果有这个属性,那么 toLowerCase 可以调用吗?
  4. 如果 message 以及它的属性都是可以调用的,那么分别返回什么?

在编写 JavaScript 代码的时候,这些问题的答案经常需要我们自己记在脑子里,而且我们必须得祈祷自己处理好了所有细节。

TypeScript
const message = "Hello World!"; // 假设 message 是这样定义的。
message.toLowerCase(); // 将会得到一个所有字母都是小写的字符串。
message(); // TypeError: message is not a function
const message = "Hello World!"; // 假设 message 是这样定义的。
message.toLowerCase(); // 将会得到一个所有字母都是小写的字符串。
message(); // TypeError: message is not a function

如果可以避免这样的错误就好了。当我们执行代码的时候,JavaScript 运行时会计算出值的类型 —— 这种类型有什么行为和功能,从而决定采取什么措施。

使用一个静态的类型系统,在代码实际执行前预测代码的行为。-- 静态类型检查

静态类型检查

  • 静态类型系统描述了程序运行时值的结构和行为。
  • TypeScript 这样的静态类型检查器会利用类型系统提供的信息,并在事态发展不对劲的时候告知我们。
TypeScript
const message = "hello!";
message();
// message() 它会在我们执行代码之前首先抛出一个错误。
// This expression is not callable. Type 'String' has no call signatures.
const message = "hello!";
message();
// message() 它会在我们执行代码之前首先抛出一个错误。
// This expression is not callable. Type 'String' has no call signatures.

非异常失败

JavaScript中访问对象上不存在的属性时,它会返回的是值 undefined。

我们需要一个静态类型系统(TypeScript)来告诉我们,哪些代码在这个系统中被标记为错误的代码 —— 即使它是不会马上引起错误的“有效” JavaScript 代码。

TypeScript
const user = {
    name: 'Daniel',
    age: 26,
};
user.location; // 返回 undefined

// --------------------------------

const user = {
  name: "Daniel",
  age: 26,
};
user.location; 
// user.location; 它会在我们执行代码之前首先抛出一个错误。
// Property 'location' does not exist on type '{ name: string; age: number; }'.
const user = {
    name: 'Daniel',
    age: 26,
};
user.location; // 返回 undefined

// --------------------------------

const user = {
  name: "Daniel",
  age: 26,
};
user.location; 
// user.location; 它会在我们执行代码之前首先抛出一个错误。
// Property 'location' does not exist on type '{ name: string; age: number; }'.

TypeScript 也的确可以捕获到很多合法的 bug。

  1. 拼写错误:
TypeScript
const announcement = "Hello World!";
announcement.toLocaleLowercase(); // ×  你需要花多久才能注意到拼写错误?
announcement.toLocaleLowerCase(); // ✔  实际上正确的拼写是这样的
const announcement = "Hello World!";
announcement.toLocaleLowercase(); // ×  你需要花多久才能注意到拼写错误?
announcement.toLocaleLowerCase(); // ✔  实际上正确的拼写是这样的
  1. 未调用的函数:
TypeScript
function flipCoin() {
  return Math.random < 0.5;  //  应该是 Math.random()
}
// 报错:Operator '<' cannot be applied to types '() => number' and 'number'.
function flipCoin() {
  return Math.random < 0.5;  //  应该是 Math.random()
}
// 报错:Operator '<' cannot be applied to types '() => number' and 'number'.
  1. 基本的逻辑错误:
TypeScript
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.
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 的编辑器可以通过“快速修复”功能自动修复错误,重构产生易组织的代码。同时,它还具备有效的导航功能,能够让我们跳转到某个变量定义的地方,或者找到对于给定变量的所有引用。

编译器 —— tsc

npm install -g typescript
npm install -g typescript

这将全局安装 TypeScript 的编译器 tsc。如果你更倾向于安装在本地的 node_modules 文件夹中,那你可能需要借助 npx 或者类似的工具才能便捷地运行 tsc 指令。

  1. 新建一个空文件夹,尝试编写第一个 TypeScript 程序:hello.ts :
TypeScript
// hello.ts 文件
console.log('Hello world!');
// hello.ts 文件
console.log('Hello world!');
  1. 让我们运行 typescript 安装包自带的 tsc 指令进行类型检查。
TypeScript
tsc hello.ts // 运行命令
tsc hello.ts // 运行命令
  1. 如果我们查看当前目录,会发现除了 hello.ts 文件外还有一个 hello.js 文件。
  2. 在这个例子中,TypeScript 几乎没有需要转译的内容,所以转译前后的代码看起来一模一样。 TypeScript 始终保持缩进,关注跨行的代码,并且会尝试保留注释。
  3. 如果我们刻意引入了一个类型检查错误呢?重写一下 hello.ts :
TypeScript
// hello.ts 文件
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date}!`);
}
greet("Brendan");
// hello.ts 文件
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date}!`);
}
greet("Brendan");
  1. 如果我们再次执行 tsc hello.ts,那么会注意到命令行抛出了一个错误!
Expected 2 arguments, but got 1.
Expected 2 arguments, but got 1.

报错时仍产出文件

举个例子,想象你现在正把 JavaScript 代码迁移到 TypeScript 代码,并产生了很多类型检查错误。

最后,你不得不花费时间解决类型检查器抛出的错误,但问题在于,原始的 JavaScript 代码本身就是可以运行的!为什么把它们转换为 TypeScript 代码之后,反而就不能运行了呢?

TypeScript 并不会对你造成阻碍。当然,随着时间的推移,你可能希望对错误采取更具防御性的措施,同时也让 TypeScript 采取更加严格的行为。

在这种情况下,你可以开启 noEmitOnError 编译选项。尝试修改你的 hello.ts 文件,并使用参数去运行 tsc 指令:

tsc --noEmitOnError hello.ts
tsc --noEmitOnError hello.ts

现在你会发现,hello.js 没有再发生改动了。

因为 noEmitOnError 属性用于在代码中存在类型错误时不生成 JavaScript 文件。

const a = 1;

显式类型

有了类型注解之后gree的参数显示的定义为:string, Date对象,TypeScript 就能告诉我们,哪些情况下对于 greet 的调用可能是不正确的。

TypeScript
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'.
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'.

因为在 JavaScript 中直接调用 Date() 返回的是 string。需要通过 new Date() 去构造一个 Date,则可以如预期那样返回一个 Date 对象

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

我们并不总是需要显式地进行类型注解。在很多情况下,即使省略了类型注解,TypeScript 也可以为我们推断出(或者“搞清楚”)类型。

TypeScript
let msg = "hello there!"; //  msg: string; TypeScript 也可以为我们推断出msg类型为string
let msg = "hello there!"; //  msg: string; TypeScript 也可以为我们推断出msg类型为string

擦除类型

通过 tsc 将上面的 greet 函数编译成 JavaScript 后会发生什么事:

TypeScript
"use strict";
function greet(person, date) {
  console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
greet("Maddison", new Date());
"use strict";
function greet(person, date) {
  console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
greet("Maddison", new Date());
  1. 我们的 person 和 date 参数的类型注解不见了。
  2. 我们的“模板字符串” —— 使用反引号(`)包裹的字符串 —— 变成了通过 + 拼接的普通字符串。

因为 类型注解并不属于 JavaScript(或者专业上所说的 ECMAScript)的内容,所以没有任何浏览器或者运行时能够直接执行不经处理的 TypeScript 代码。

这也是为什么 TypeScript 首先需要一个编译器 —— 它需要经过编译,才能去除或者转换 TypeScript 独有的代码,从而让这些代码可以在浏览器上运行。

TIP

**记住:**类型注解永远不会改变你的程序在运行时的行为

降级

上面的另一个变化,就是我们的模板字符串从:

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

被重写为:

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

为什么会这样?

模板字符串是 ECMAScript 2015(或者 ECMAScript6、ES2015、ES6 等)引入的新特性。

TypeScript 可以将高版本 ECMAScript 的代码重写为类似 ECMAScript3 或者 ECMAScript5 (也就是 ES3 或者 ES5)这样较低版本的代码。

类似这样将更新或者“更高”版本的 ECMAScript 向下降级为更旧或者“更低”版本的代码,就是所谓的降级

默认情况下,TypeScript 会转化为 ES3 代码,这是一个非常旧的 ECMAScript 版本。我们可以使用 target 选项将代码往较新的 ECMAScript 版本转换。

通过使用 --target es2015 参数进行编译,我们可以得到 ECMAScript2015 版本的目标代码,这意味着这些代码能够在支持 ECMAScript2015 的环境中执行。

因此,运行 tsc --target es2015 hello.ts 之后,我们会得到如下代码:

TypeScript
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", new Date());
function greet(person, date) {
  console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", new Date());

TIP

虽然默认的目标代码采用的是 ES3 语法,但现在浏览器大多数都已经支持 ES2015 了。

所以,大多数开发者可以安全地指定目标代码采用 ES2015 或者是更高的 ES 版本,除非你需要着重兼容某些古老的浏览器。

严格性

不同的用户会由于不同的理由去选择使用 TypeScript 的类型检查器。

一些用户寻求的是一种更加松散、可选的开发体验,他们希望类型检查仅作用于部分代码,同时还可享受 TypeScript 提供的功能。

这也是 TypeScript 默认提供的开发体验,类型是可选的,推断会使用最松散的类型,对于潜在的 null/undefined 类型的值也不会进行检查。

就像 tsc 在编译报错的情况下仍然能够正常产出文件一样,这些默认的配置会确保不对你的开发过程造成阻碍。

如果你正在迁移现有的 JavaScript 代码,那么这样的配置可能刚好适合。

另一方面,大多数的用户更希望 TypeScript 可以快速地、尽可能多地检查代码,这也是这门语言提供了严格性设置的原因。

这些严格性设置将静态的类型检查从一种切换开关的模式(对于你的代码,要么全部进行检查,要么完全不检查)转换为接近于刻度盘那样的模式。

TypeScript 有几个和类型检查相关的严格性设置,它们可以随时打开或关闭,如若没有特殊说明,我们文档中的例子都是在开启所有严格性设置的情况下执行的。

CLI 中的 strict 配置项,或者 tsconfig.json 中的 "strict: true" 配置项,可以一次性开启全部严格性设置。但我们也可以单独开启或者关闭某个设置。

你的程序使用越多的类型配置,那么在验证和工具上你的收益就越多,这意味着在编码的时候你会遇到越少的 bug。

noImplicitAny

采用最宽泛的类型:any。这并不是一件最糟糕的事情 —— 毕竟,使用 any 类型基本就和纯 JavaScript 一样了。使用 any 通常会和使用 TypeScript 的目的相违背。

启用 noImplicitAny 配置项,在遇到被隐式推断为 any 类型的变量时就会抛出一个错误。

strictNullChecks

默认情况下,null 和 undefined 可以被赋值给其它任意类型。

strictNullChecks 配置项让处理 null 和 undefined 的过程更加明显,让我们不用担心自己是否忘记处理 null 和 undefined。