TypeScript与Babel

很多现有的 JavaScript 项目已经在使用 Babel 作为代码转译工具,而 TypeScript 也具有代码转译的功能。在将现有的 JavaScript 项目迁移到 TypeScript 时,开发者既可以选择继续使用 Babel 作为代码转译工具,也可以选择使用 TypeScript 来转译代码。

在本节中,将简要介绍 Babel 与 TypeScript 的代码转译功能,并通过一个示例来演示如何在 TypeScript 工程中使用 Babel 来转译代码。

Babel

Babel 是诞生于 2014 年的开源项目,它是一个比较流行的 JavaScript 程序转译器。Babel 能够对输入的 JavaScript 代码进行解析,并在保证功能不变的情况下重新生成 JavaScript 代码。

Babel 的原名是 6to5。最初,它是用来将符合 ES6 标准的代码转译为符合 ES5 标准的代码。这样一来使用最新语言特性编写的程序也能够在低版本的运行环境中运行。

但自从 2015 年发布了 ES6 标准以来,每一年都会有新版本的 ECMAScript 标准发布并以年份命名。例如,ES6 也叫作 ES2015,还有之后的 ES2016、ES2017 等。于是,Babel 的功能也不仅限于转译 ES6 代码,它还能够转译其他 ECMAScript 版本的代码。与此同时,Babel 的架构也进行了演进,变得更加模块化并且提供了插件系统,开发者可以自定义代码转译的行为。基于以上种种原因,6to5 已经不再是一个恰当的名字,最终被更名为 Babel

现如今,Babel 的主要用途是将 ES6 及以上版本的 JavaScript 代码转译为兼容某一运行环境的 JavaScript 代码。例如,它既可以将 ES6 代码转译为 ES5 代码,也可以将 ES7 代码转译为兼容某一浏览器的代码,如 Microsoft Internet Explorer 浏览器。从 Babel 7 版本开始,Babel 内置了对 TypeScript 语言的支持,因此也能够将 TypeScript 代码转译为兼容某一运行环境的 JavaScript 代码。

例如,有如下的 TypeScript 代码:

[1, 2, 3].map((n: number) => n + 1);

这段代码使用了箭头函数,能够在支持 ES6 的运行环境中运行,但在 ES5 及以下版本的运行环境中则无法运行。在这种情况下,我们可以使用 Babel 将上述代码转译为符合 ES5 规范的 JavaScript 代码。Babel 能够将箭头函数转译为 ES5 规范中定义的函数表达式,从而这段程序能够在仅支持 ES5 的环境中运行。示例如下:

[1, 2, 3].map(function(n) {
  return n + 1;
});

TypeScript编译器

TypeScript 编译器具有以下两个主要功能:

  • 能够对 TypeScript 和 JavaScript 代码进行静态类型检查。

  • 能够将 TypeScript 和 JavaScript 代码转译为 JavaScript 代码。

由此可以看出,TypeScript 编译器与 Babel 的相同之处在于两者都能够将 TypeScript 和 JavaScript 代码转译为 JavaScript 代码,而两者的不同之处在于 TypeScript 编译器还能够对 TypeScript 和 JavaScript 代码进行静态类型检查。

转译TypeScript

将 TypeScript 代码转译为 JavaScript 代码是 TypeScript 编译器的主要功能之一。例如,index.ts 文件使用了 ES6 规范中定义的箭头函数语法,如下所示:

[1, 2, 3].map((n: number) => n + 1);

通过运行以下 tsc 编译命令能够将上述 TypeScript 代码编译为符合 ES5 规范的 JavaScript 代码,示例如下:

tsc index.ts --target ES5

编译生成的 JavaScript 代码如下:

[1, 2, 3].map(function(n) {
    return n + 1;
});

TypeScript 编译器还会对 TypeScript 代码进行类型检查,例如检查 map 方法的类型。此例中,经过 TypeScript 编译器处理后的 JavaScript 代码与使用 Babel 转译后的 JavaScript 代码是相同的。

如果在运行 tsc 编译命令时启用了 --allowJs 编译选项,那么 TypeScript 编译器也能够转译 JavaScript 文件。例如,有 index.js 文件的内容如下:

[1, 2, 3].map(n => n + 1);

运行 tsc 编译命令将 index.js 编译为 ES5 代码,示例如下:

tsc index.js --target ES5 --allowJs --outFile index.out.js

编译生成的 index.out.js 文件的内容如下:

[1, 2, 3].map(function(n) {
    return n + 1;
});

虽然 TypeScript 编译器包含了 Babel 的主要功能,但两者并不是对立的关系。TypeScript 和 Babel 可以结合使用,TypeScript 负责静态类型检查而 Babel 则负责转译 JavaScript 代码。

该模式的一个应用场景是对现有项目进行改造。例如,有一个使用 JavaScript 开发的 Web 应用,它已经在依赖于 Babel 将 JavaScript 源码转译为兼容旧版本浏览器环境的代码。现在,我们想要为该程序添加静态类型检查。那么,只需要在原先的 Babel 工作流中增加一步,使用 TypeScript 编译器对代码进行静态类型检查即可。

关于编译器与转译器

在此前的章节中,我们多次提到了编译器与转译器,本节将对两者做一个简短的介绍。

编译器指的是能够将高级语言翻译成低级语言的程序。编译器的典型代表是 C 语言编译器,它能够将 C 语言程序翻译为低级的机器语言。

转译器通常是指能够将高级语言翻译成高级语言的程序,即在同一抽象层次上进行源代码的翻译。转译器的典型代表是 Babel,它能够将符合 ES6 规范的代码翻译为符合 ES5 规范的代码,其输入和输出均为 JavaScript 语言的代码。

有时候 TypeScript 编译器也称作 TypeScript 转译器,因为它是在 TypeScript 语言与 JavaScript 语言之间进行翻译的,而这两种编程语言拥有相近的抽象层次。

实例演示

本节将通过一个简单的例子来演示如何将 TypeScript 与 Babel 结合使用。此例中,TypeScript 编译器负责对代码进行静态类型检查,然后 Babel 负责将 TypeScript 代码转译为可执行的 JavaScript 代码,具体如图9-1所示。

image 2024 02 07 22 49 07 372
Figure 1. 图9-1 TypeScript与Babel

安装Node.js

在使用 TypeScript 和 Babel 之前,我们需要先安装 Node.js。在 Node.js 的官方网站上下载适合当前操作系统的软件安装包并安装。在安装 Node.js 时会自动安装 npm 工具,即 Node.js 包管理器,如图9-2所示。

image 2024 02 07 22 50 13 762
Figure 2. 图9-2 Node.js下载页面

安装完成后,打开命令行窗口并运行 node -v 命令来验证是否安装成功。如果成功地安装了 Node.js,那么该命令会显示出安装的 Node.js 的版本号,如 v12.18.0(见图9-3)。

image 2024 02 07 22 51 08 228
Figure 3. 图9-3 显示版本号

初始化工程

新建一个名为 ts-babel 的文件夹,该文件夹将作为示例工程的根目录。为了简便起见,我们直接在系统根目录 C:\ 下创建该文件夹。接下来,在 C:\ts-babel 目录下运行如下命令来初始化工程:

npm init --yes

该命令的作用是初始化一个新的 npm 包,--yes 选项的作用是使用默认配置来初始化 npm 包。

在运行该命令后,将在 C:\ts-babel 目录下生成一个 package.json 文件。接下来,从该文件中删除此例中不关注的配置项,修改后的 package.json 文件如下:

{
    "name": "ts-babel",
    "version": "1.0.0"
}

每个 npm 包都包含一个 package.json 文件。该文件相当于一个配置文件,它记录了当前 npm 包的基本信息。npm 工具能够读取并使用 package.json 文件中的信息。

现在,C:\ts-babel 目录的结构如下:

C:\ts-babel
`-- package.json

安装TypeScript

在 C:\ts-babel 目录下运行如下命令来安装 TypeScript 语言:

npm install --save-dev typescript

该命令用于安装某个 npm 包及其依赖的包。其中,typescript 是 TypeScript 语言发布到 npm 时使用的包名。

运行该命令会在 C:\ts-babel\node_modules 目录下安装 TypeScript 语言。现在,C:\ts-babel 目录的结构如下:

C:\ts-babel
|-- node_modules
|   `-- typescript
|-- package.json
`-- package-lock.json

其中,package-lock.json 文件是由 npm 工具自动生成的,它的作用是锁定依赖的 npm 包的具体版本号,这样就能够确保每次执行安装命令时都会安装相同版本的 npm 包。

此时,package.json 文件的内容如下:

{
    "name": "ts-babel",
    "version": "1.0.0",
    "devDependencies": {
        "typescript": "^3.8.2"
    }
}

能够看到,typescript 被添加到了 devDependencies 属性中作为一项开发时的依赖;^3.8.2 表示依赖的 TypeScript 版本号,它采用了语义化版本格式。

在 C:\ts-babel 目录下,运行下列命令来验证 TypeScript 是否安装成功:

npx tsc -v

若安装成功,则会显示安装的 TypeScript 的版本号,如 Version 3.8.2。请注意,此例中我们使用了 npx 命令来确保使用的是安装在 C:\ts-babel\node_modules 目录下的 TypeScript。

配置TypeScript

在 C:\ts-babel 目录下新建一个 tsconfig.json 配置文件,它的内容如下:

{
    "compilerOptions": {
        "target": "ES5",
        "outDir": "lib",
        "strict": true,
        "isolatedModules": true
    }
}

此时,C:\ts-babel 目录的结构如下:

C:\ts-babel
|-- node_modules
|   `-- typescript
|-- package.json
|-- package-lock.json
`-- tsconfig.json

安装Babel

对于 JavaScript 程序开发而言,工具和环境配置从来不是一件容易的事情。为了简化配置流程,Babel 与 TypeScript 开发团队进行了合作,在 Babel 7 版本中提供了内置的 TypeScript 支持。

此例中,将使用 Babel 7 进行演示。我们需要安装下列 npm 包:

  • @babel/core 提供了 Babel 核心的转译代码的功能。

  • @babel/cli 提供了 Babel 命令行工具。

  • @babel/preset-env 提供了自动检测待转译的语法以及自动导入填充脚本(polyfill)的功能。

  • @babel/preset-typescript,它是 Babel 与 TypeScript 结合的关键,它提供了从 TypeScript 代码中删除类型相关代码的功能,如类型注解。

接下来,在 C:\ts-babel 目录下使用如下命令来安装上述 npm 包。示例如下:

npm install @babel/core \
            @babel/cli \
            @babel/preset-env \
            @babel/preset-typescript --save-dev

在运行了上述命令之后,C:\ts-babel 目录的结构如下:

C:\ts-babel
|-- node_modules
|   |-- <省略了一部分代码包>
|   |-- @babel
|   `-- typescript
|-- package.json
|-- package-lock.json
`-- tsconfig.json

package.json 文件的内容如下:

{
    "name": "ts-babel",
    "version": "1.0.0",
    "devDependencies": {
        "@babel/cli": "^7.8.4",
        "@babel/core": "^7.8.4",
        "@babel/preset-env": "^7.8.4",
        "@babel/preset-typescript": "^7.8.4",
        "typescript": "^3.8.2"
    }
}

在 C:\ts-babel 目录下运行下列命令来验证 Babel 是否安装成功。示例如下:

npx babel --version

如果安装成功,则会显示出安装的 Babel 的版本号,如 7.10.4 (@babel/core 7.10.4)。

配置Babel

tsconfig.json 配置文件类似,.babelrc.json 文件是 Babel 的配置文件。在 C:\ts-babel 目录下新建一个 .babelrc.json 配置文件,它的内容如下:

{
    "presets": [
        "@babel/env",
        "@babel/typescript"
    ]
}

现在,C:\ts-babel 目录的结构如下:

C:\ts-babel
|-- node_modules
|   |-- <省略了一部分代码包>
|   |-- @babel
|   `-- typescript
|-- .babelrc.json
|-- package.json
|-- package-lock.json
`-- tsconfig.json

添加TypeScript源文件

在 C:\ts-babel 目录下新建一个 src 文件夹,然后在 C:\ts-babel\src 目录下新建一个 index.ts 文件,它的内容如下:

export function toUpperCase(strs: string[]): string[] {
    return strs.map((str: string) => str.toUpperCase());
}

此时,C:\ts-babel 目录的结构如下:

C:\ts-babel
|-- node_modules
|   |-- <省略了一部分代码包>
|   |-- @babel
|   `-- typescript
|-- src
|   `-- index.ts
|-- .babelrc.json
|-- package.json
|-- package-lock.json
`-- tsconfig.json

配置静态类型检查

TypeScript 编译器提供了一个 --noEmit 编译选项。当启用该编译选项时,编译器只会对代码进行静态类型检查,而不会将 TypeScript 代码转译为 JavaScript 代码。

在 C:\ts-babel 目录下,运行 tsc 编译命令并启用 --noEmit 编译选项,示例如下:

npx tsc --noEmit

运行该命令能够对 TypeScript 代码进行类型检查,但不会生成任何 JavaScript 文件。

我们可以将该命令添加为 npm 脚本来方便之后进行调用。在 package.json 文件的 scripts 属性中定义一个 type-check 脚本,示例如下:

{
    "name": "ts-babel",
    "version": "1.0.0",
    "scripts": {
        "type-check": "tsc --noEmit"
    },
    "devDependencies": {
        "@babel/cli": "^7.8.4",
        "@babel/core": "^7.8.4",
        "@babel/preset-env": "^7.8.4",
        "@babel/preset-typescript": "^7.8.4",
        "typescript": "^3.8.2"
    }
}

在 C:\ts-babel 目录下,通过如下方式来运行 type-check 脚本:

npm run type-check

配置转译JavaScript

在 C:\ts-babel 目录下,运行 babel 命令将 TypeScript 代码转译为 JavaScript 代码。示例如下:

npx babel src \
          --out-dir lib \
          --extensions \".ts,.tsx\" \
          --source-maps inline

同理,我们也可以将该命令添加为 npm 脚本来方便之后进行调用。在 package.json 文件的 scripts 属性中定义一个 build 脚本,示例如下:

{
    "name": "ts-babel",
    "version": "1.0.0",
    "scripts": {
        "type-check": "tsc --noEmit",
        "build": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline"
    },
    "devDependencies": {
        "@babel/cli": "^7.8.4",
        "@babel/core": "^7.8.4",
        "@babel/preset-env": "^7.8.4",
        "@babel/preset-typescript": "^7.8.4",
        "typescript": "^3.8.2"
    }
}

在 C:\ts-babel 目录下,通过如下方式来运行 build 脚本:

npm run build

通过上述的配置,我们让 Babel 去转译 C:\ts-babel\src 目录下的 TypeScript 代码,并将生成的 JavaScript 代码放在 C:\ts-babel\lib 目录下。运行该命令后 C:\ts-babel 目录的结构如下:

C:\ts-babel
|-- lib
|   `-- index.js
|-- node_modules
|   |-- <省略了一部分代码包>
|   |-- @babel
|   `-- typescript
|-- src
|   `-- index.ts
|-- .babelrc.json
|-- package.json
|-- package-lock.json
`-- tsconfig.json

生成的 index.js 文件的内容如下:

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.toUpperCase = toUpperCase;

function toUpperCase(strs) {
  return strs.map(function (str) {
    return str.toUpperCase();
  });
}
//# sourceMappingURL=data:application/json;...

注意事项

虽然 Babel 能够转译绝大部分的 TypeScript 代码,但仍存在一些语言结构无法很好地处理。例如:

  • 命名空间。

  • <T> 类型断言语法,但可以使用 x as T 语法来代替。

  • 常量枚举 const enum

  • 跨文件声明的枚举,在 TypeScript 中会被合并为一个声明。

  • 旧的导入导出语法,如:

    • export =

    • import =

在编译 TypeScript 时,可以启用 --isolatedModules 编译选项,它的作用是当编译器发现无法被正确处理的语言结构时给出提示。

小结

本节介绍了 TypeScript 与 Babel 的功能对比,以及如何将两者结合使用。

如果想要为程序添加静态类型检查的功能,则需要使用 TypeScript。如果想要将 TypeScript 或 JavaScript 代码转译为兼容某一运行环境的 JavaScript,则既可以使用 TypeScript 也可以使用 Babel

如果一个项目已经在使用 TypeScript,则推荐继续使用 TypeScript 编译器来转译代码;若当前项目已经在使用 Babel,则可以继续使用 Babel 来转译代码,同时使用 TypeScript 编译器进行静态类型检查。如果计划开始一个全新的项目并且选用 TypeScript 进行开发,则不需要在项目一开始就引入 Babel