你已经了解了 JavaScript 与 TypeScript 之间的关系,也看到了 TypeScript 如何改善开发者的工作体验。但现在,让我们再深入一点。在本章中,你将把 TypeScript CLI 运行起来,并看看它在开发流水线中处于什么位置。我们将讨论在浏览器中运行 TypeScript 所面临的问题、TypeScript 如何变成 JavaScript 的过程,以及如何将 TypeScript 与现代前端框架集成起来。
作为示例,我们会使用 TypeScript 向你展示如何构建一个 Web 应用。需要注意的是,只要是 JavaScript 可以使用的地方,TypeScript 基本上也都可以使用——无论是在 Node、Electron、React Native,还是其他任何应用中。
浏览器中的 TypeScript 问题
来看下面这个 TypeScript 文件 example.ts,其中包含一个 run 函数,用于在控制台打印一条消息:
const run = (message: string) => {
console.log(message);
};
run("Hello!");
与 example.ts 文件配套的,还有一个基础的 index.html 文件,它通过 script 标签引用了这个 example.ts 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My App</title>
</head>
<body>
<script src="example.ts"></script>
</body>
</html>
然而,当你在浏览器中打开 index.html 时,会在控制台里看到这样一条错误:
Unexpected token ':'
TypeScript 文件里并没有出现红色波浪线,那为什么还会这样呢?这是因为:浏览器无法运行 TypeScript。浏览器(以及 Node.js 之类的其他运行时)本身并不理解 TypeScript;它们理解的只有 JavaScript。对于 run 函数来说,函数声明中 message 后面的 : string 并不是合法的 JavaScript 语法:
const run = (message: string) => {
// : string 不是合法的 JavaScript!
console.log(message);
};
如果你把 : string 删除,代码看起来的确会更像 JavaScript,但这时 TypeScript 又会在 message 下方报错:
const run = (message) => {}; // message 下方出现红色波浪线
把鼠标悬停在 VS Code 中这条红色波浪线上,你会看到错误信息,提示 message 隐式地具有 any 类型。我们之后会详细解释这是什么意思;但现在你只需要明白一点:example.ts 文件里原本包含了浏览器无法理解的语法,而把这些语法删掉之后,TypeScript CLI 又会不满意。要让浏览器理解你的 TypeScript 代码,你就必须先把它转换成 JavaScript。
TypeScript 的转译
负责把 TypeScript 转换成 JavaScript 的,就是 TypeScript 的命令行工具 tsc。安装 TypeScript 时,这个 CLI 会自动一起安装;但在真正使用它之前,你需要先把 TypeScript 项目初始化好。
为此,请打开终端,并进入 example.ts 和 index.html 所在目录的父目录。为了再次确认 TypeScript 已被正确安装,可以在终端中运行 tsc --version。如果终端输出了版本号,就说明一切正常。否则,请运行下面这条命令全局安装 TypeScript:
pnpm add -g typescript
当终端已经切换到正确目录、并且 TypeScript 也安装完成后,你就可以开始初始化 TypeScript 项目了。
初始化 TypeScript 项目
为了让 TypeScript 知道该如何转译你的代码,你需要在项目根目录下创建一个 tsconfig.json 文件。运行下面这条命令来生成它:
tsc --init
在 tsconfig.json 文件里,你会看到一些很有用的初始配置选项,以及许多被注释掉的其他可选项。
目前,我们先使用默认配置即可:
// tsconfig.json 片段
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
有了 tsconfig.json 文件之后,你就可以开始进行转译了。
运行 tsc
如果你在终端里直接运行 tsc,而不传入任何参数,它就会使用 tsconfig.json 中的默认配置,把项目里的所有 TypeScript 文件转译成 JavaScript:
tsc
在这个例子里,example.ts 文件中的 TypeScript 代码会被转成 example.js 中的 JavaScript 代码。在 example.js 中,TypeScript 语法已经被转译成普通的 JavaScript:
// 在 example.js 文件中
"use strict";
const run = (message) => {
console.log(message);
};
run("Hello!");
现在你已经有了 JavaScript 文件,就可以修改 index.html,让它引用 example.js,而不再是 example.ts:
// 在 index.html 中
<script src="example.js"></script>
这时再在浏览器中打开 index.html,控制台里就会正常输出预期的 "Hello!",而不会再有任何错误了。
TypeScript 会改变你的 JavaScript 吗?
看看生成出来的 JavaScript 文件,你会发现它和原来的 TypeScript 代码几乎没什么区别:
"use strict";
const run = (message) => {
console.log(message);
};
run("Hello!");
它去掉了 run 函数中的 : string,同时在文件顶部添加了 "use strict";;除此之外,代码基本完全相同。
TypeScript 的一个关键设计原则是:它只是在 JavaScript 之上增加一层很薄的语法,而不会改变代码本身的运行方式。它不会添加运行时校验,也不会试图优化代码性能。它只是加上类型信息,以改善你的开发体验;等到真正要把代码转换成 JavaScript 时,再把这些类型移除掉。
注意
这个设计原则也有一些例外,例如 enums、namespaces 以及类参数属性(class parameter properties),不过这些内容我们后面再讲。
关于版本控制的一点说明
你已经成功地把 TypeScript 代码转译成了 JavaScript,但与此同时,项目里也新增了一个文件。你可以在项目根目录下添加一个 .gitignore 文件,并在里面写入 *.js,这样这些生成出来的 JavaScript 文件就不会被加入版本控制。
更重要的是,这样做也向仓库中的其他开发者传达了一个明确的信号:真正的源码是 *.ts 文件,JavaScript 只是由它们生成出来的产物。
以 watch 模式运行 TypeScript
你可能已经注意到了:如果你修改了 TypeScript 文件,要想在浏览器中看到效果,就必须重新执行一次 tsc。这件事很快就会变得很烦。你甚至可能忘了重新执行,然后纳闷为什么浏览器里看不到你的改动。
幸运的是,TypeScript CLI 提供了一个 --watch 标志,可以在你保存文件时自动重新编译 TypeScript 文件:
tsc --watch
为了直观看到它的效果,可以在 VS Code 中把 example.ts 和 example.js 并排打开。如果你把 example.ts 中传给 run 函数的消息改成别的内容,就会看到 example.js 自动同步更新。
TypeScript CLI 中的错误
如果 tsc 在编译过程中遇到错误,它会在终端里显示错误信息,同时在 VS Code 中对出错的文件标出红色波浪线。比如,试着把 example.ts 中 run 函数的 message: string 改成 message: number:
const run = (message: number) => {
console.log(message);
};
run("Hello world!"); // "Hello world!" 下方出现红色波浪线
这时,终端中的 tsc 应该会显示类似下面的错误:
// 在终端中
Argument of type 'string' is not assignable to parameter of type 'number'.
run("Hello world!");
Found 1 error. Watching for file changes.
如果你再把它改回 message: string,错误就会消失,同时 example.js 也会再次自动更新。
以 watch 模式运行 tsc,不仅可以自动编译 TypeScript 文件,也能在你编写代码时及时捕获错误。它在大型项目中尤其有用,因为它会检查整个项目。而这与 IDE 不同,IDE 通常只显示当前打开文件中的错误。
TypeScript 与现代框架
到目前为止,这套流程还很简单:一个 TypeScript 文件、一条 tsc --watch 命令,再加一个生成出来的 JavaScript 文件。但如果你要真正构建一个前端应用,还需要做更多事情:处理 CSS、代码压缩、打包等等。TypeScript 并不能帮你完成所有这些工作。
幸运的是,很多前端框架和工具链可以提供帮助。Vite 就是一个典型例子。它不仅能把 .ts 文件转译为 .js 文件,还提供了一个带有热模块替换(HMR) 能力的开发服务器。使用 HMR 时,你改动代码后,可以直接在浏览器中看到变化,而不需要手动刷新页面。
但这里也有一个缺点:虽然 Vite 和其他工具能够完成 TypeScript 到 JavaScript 的转译,但它们默认并不会进行类型检查。这意味着,你的代码里即使引入了类型错误,Vite 依然会继续运行开发服务器,却不会提醒你。它甚至可能让带着错误的代码一路进入生产环境,因为它根本无从判断。所以,你依然需要 TypeScript CLI 来捕获这些错误。只不过,既然 Vite 已经负责了代码转译,你就不需要再让 TypeScript 同时做这件事了。
注意
如今,一些 JavaScript 服务端运行时,比如 Node 和 Bun,也已经原生支持 TypeScript。但它们的支持方式和 Vite 类似:只提供转译,不做类型检查。你仍然需要借助 tsc --watch 才能获得类型检查能力。
把 TypeScript 当作 Linter 使用
幸运的是,你可以配置 TypeScript CLI,让它只负责类型检查,而不去干扰其他工具。在 tsconfig.json 文件中,有一个选项叫做 noEmit,它用于告诉 tsc 是否要输出 JavaScript 文件。
把 noEmit 设置为 true 后,运行 tsc 时就不会再生成任何 JavaScript 文件。这样,TypeScript 的角色就更像是一个 linter(静态检查工具) ,而不是一个转译器。
这会让 tsc 这一步非常适合集成进 CI/CD 系统,因为它可以阻止那些带有 TypeScript 错误的 Pull Request 被合并。后面在本书中,我们还会更深入地讨论一些更高级的 TypeScript 配置,用于实际应用开发。
小结
本章探讨了 TypeScript 在开发流水线中的位置,以及在浏览器中运行 TypeScript 代码所面临的挑战。核心问题在于:浏览器并不理解 TypeScript 语法,它只理解 JavaScript。
因此,当你尝试在浏览器中直接运行 TypeScript 时,就会因为类型注解(例如 : string)这样的语法并不是合法 JavaScript,而遇到错误。这时,TypeScript CLI 就承担起了转译代码的职责。
tsc 命令会通过移除类型注解,并补充必要的 JavaScript 语法,把 TypeScript 文件转换成 JavaScript。你可以通过 tsc --init 初始化一个 TypeScript 项目,它会生成一个包含编译配置的 tsconfig.json 文件。
运行 tsc 会转译项目中的所有 TypeScript 文件。最终产出的 JavaScript 与原始 TypeScript 几乎完全相同,只是去掉了类型而已。TypeScript 始终遵循这样一个原则:它只是覆盖在 JavaScript 之上的一层薄薄语法,而不会改变代码的运行方式。
--watch 标志可以在文件发生改动时自动重新编译,大幅提升开发效率。同时,CLI 也会在终端中报告类型错误,帮助你在写代码的过程中及时修复问题。
现代前端工具(如 Vite)可以负责 TypeScript 的转译,但它们默认不提供类型检查。通过在 tsconfig.json 中设置 noEmit: true,你可以把 TypeScript 配置成一个专注于类型检查的工具,而把转译工作交给其他工具处理。
这种分工方式使得 TypeScript 成为 CI/CD 流水线中的理想组成部分,它能够有效阻止带有类型错误的代码进入生产环境。