开发流水线里的 TypeScript — tsc、转译与「类型只在写的时候存在」¶
这一章讲一件事: 你的
.ts文件怎么变成浏览器能跑的东西,以及这件事背后那条全书级的指导原则。 读完你会彻底明白「类型在运行时去哪了」——以及为什么这既是 TypeScript 最大的优点,也是它一切局限的根。
1. 先看现象:一个文件,两种死法(又一次)¶
主线例子还是上一章那个 run 函数,这次看它在一个具体的陷阱里怎么死两次。
你有一个 example.ts:
和一个引用它的 index.html(<script src="example.ts"></script>)。
在浏览器里打开页面,控制台报错:
为什么? 因为浏览器读不了 TypeScript——(message: string) 里那个冒号和 string,
是 TypeScript 的语法,不是 JavaScript 的语法1。
好,那把冒号删掉,变成纯 JavaScript?TypeScript 这关又过不去:
删掉 : string 后,编辑器在 message 下面画红线,报错说它「隐式地是 any 类型」2。
这就是本章的入口矛盾,也是整个 TypeScript 工作流的原点:
any 是什么这里先不展开(第 04、13 章会把它讲透),记住画面就行:
浏览器要的是「没有类型的 JS」,TypeScript 要的是「有类型的标注」,tsc 就是夹在中间那个翻译官。
2. tsc:翻译官的工作流程¶
tsc(TypeScript CLI)是负责把 TypeScript 变成 JavaScript 的工具,装 TypeScript 时自动带上3。
完整走一遍:
第 1 步:项目根目录跑 tsc --init → 生成 tsconfig.json(翻译配置)
第 2 步:配置里先认两个默认项:target(翻译成哪个 JS 版本)与 strict(严格检查)
第 3 步:跑 tsc → 每个 .ts 旁边出现一个同名 .js
第 4 步:把 index.html 里的引用从 example.ts 改成 example.js
第 5 步:浏览器打开 → 控制台打出 "Hello!",零报错
图说:tsc --init 只在项目开头跑一次;tsc 每次构建都要跑(或让它常驻,见第 4 节)。
看第 3 步的产物——这是本章最重要的一段对照:
// tsc 产出的 example.js
"use strict";
const run = (message) => {
console.log(message);
};
run("Hello!");
区别只有两处:冒号和 string 消失了;文件顶上多了一行 "use strict";(严格模式)。
除此之外逐字相同4。
补两个 --init 的实用细节。第一,tsconfig.json 里已经生效的默认值只有六项——
原书把它们整个列了出来,值得看一眼 because 它们解释了「为什么新项目编译产物
是老语法」:
// tsc --init 的默认 compilerOptions
{
"target": "es2016", // ← 默认翻译目标很老!
"module": "commonjs", // ← 默认模块格式是 CommonJS
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
target: es2016 意味着默认产物不含任何 2016 年之后进 JavaScript 的语法——
箭头函数(2015)有,可选链 ?.(2020)没有。想产出现代代码,第一步就是把它抬高;
module: commonjs 同理,和浏览器 ESM 的世界观相反(第 15 章的 module 二选一
就是来修这个默认值的)16。
第二,--init 生成的文件里还有大量被注释掉的备选——它们不是配置,是
官方塞进来的「选项菜单」,用到再解开16。
3. 指导原则:一层薄语法,且不改你的代码¶
由上面的对照,原书立起一条全书级的指导原则,值得整段记住:
TypeScript 是 JavaScript 之上的一层薄语法,但它不改变代码的工作方式。 它不加运行时校验,不优化性能。它只是加上类型、改善开发体验, 然后在编译时把类型摘掉。5
把这条原则的三个「不」拆开看,每一个都是后文的伏笔:
| 原则说「不」 | 含义 | 伏笔 |
|---|---|---|
| 不改运行行为 | 跑起来的代码和你写的逻辑一模一样 | 类型对运行时不可见(第 11 章的「撒谎账单」) |
| 不加运行时校验 | 类型对了不代表值真的对 | 网络数据、JSON 进来时类型帮不上忙 |
| 不优化性能 | 类型不是给机器加速用的 | 呼应第 01 章:类型的受益者是工具和人,不是运行时 |
原则有例外,书里也明说了:枚举(enum)、命名空间(namespace)、类参数属性(class parameter properties) 这三个特性会生成普通 JavaScript 里不存在的东西——它们会改变运行代码,所以被单独立为「只有 TypeScript 有的特性」在第 10 章审问6。
版本控制的一个约定:既然 .js 是 .ts 的翻译产物,就该在 .gitignore 里忽略 *.js——
这不只是卫生问题,更是在向所有用仓库的人宣布:.ts 才是源码的单一事实源7。
4. watch 模式:tsc 当常驻质检员¶
每次都手动跑 tsc,改两下就烦了,而且忘了跑就纳闷「浏览器里怎么没变」。
tsc --watch 让 tsc 常驻:你一保存,它立刻重新翻译8。
tsc 在终端报的错,和 IDE 里的红线是同一份检查的两个出口:
比如你把 message: string 改成 message: number,watch 模式下的终端立刻显示
Argument of type 'string' is not assignable to parameter of type 'number',
改回来,报错消失、.js 自动更新9。
原书指出 watch 模式的一个独特价值:它检查整个项目,而 IDE 只显示当前打开文件的错误10。 项目一大,这个差别就是「漏报」和「不漏报」的差别。
把终端里的完整报错剧本走一遍,这个「常驻质检员」的工作方式就具体了。
你故意把 message: string 改成 message: number,保存:
// 终端里立刻出现:
Argument of type 'string' is not assignable to parameter of type 'number'.
run("Hello world!");
Found 1 error. Watching for file changes. ← 最后这行是关键
图说:"Watching for file changes" 表示 tsc 没退出、在等你的下一次保存;
报错修掉,这行流水继续,旁边的 .js 自动更新——质检员不干翻译之外的事[^17]。
而 VS Code 里那份「出错的文件被画上红线」的提示,和终端里的报错是同一次检查、 两个出口——不存在「终端认了、编辑器不认」的两份真相17。
5. 现代框架时代:Vite 能转译,但不查类型¶
真实的前端项目不会只用一个 tsc——还要处理 CSS、压缩、打包。
以 Vite 为代表的现代工具链把这些全包了,顺带也把 .ts 翻成 .js,
还提供热模块替换(HMR):改代码不用刷新页面,浏览器里立刻变11。
但这里有一个全书最要紧的现代陷阱,原书是敲着黑板讲的:
Vite 这类工具「支持 TypeScript」的意思是只做转译、不做类型检查。 你的代码里有类型错误,Vite 照样高高兴兴跑开发服务器,一个字都不提醒; 它甚至不会拦着你把错误带进生产环境——因为它根本不知道有错。12
书里补的注把这个坑又挖深了一层:Node、Bun 这些运行时现在也「原生支持 TypeScript」了, 但性质一样——只转译、不查类型13。
所以结论很硬:只要你用了任何「原生支持 TypeScript」的工具,就仍然需要 tsc 做类型检查。 「能跑」和「类型对」是两份独立的报告,前者由 Vite/Node 给,后者只能 tsc 给。
6. noEmit:tsc 的第二种形态——只查不发的 linter¶
既然 Vite 负责产出 .js,tsc 再产一份就是重复劳动。配置里的 noEmit: true
告诉 tsc:「只检查,不产出文件」14。这时 tsc 的角色从翻译官变成质检员——
一个挂在 TypeScript 身上的 linter(代码检查器)。
这个形态恰好是 CI/CD 流水线想要的:tsc 这一步放进持续集成,
带着类型错误的合并请求就会被拦下15。「类型检查」和「产出代码」就此解耦:
转译归 Vite,检查归 tsc --noEmit,各管一段。
现代前端流水线的分工:
Vite —— 开发服务器、HMR、转译、打包 (只管「能跑」)
tsc --noEmit —— 全项目类型检查 (只管「类型对」)
CI —— 跑 tsc,有错就拦下合并 (把检查变成门禁)
图说:本章开头那个「两套语法各自执法」的矛盾,到这里被拆成了两条独立流水线。
7. 作者的判断与证据(分开标)¶
书里有据的:
- 「浏览器只认 JS、冒号是非法记号」——可当场复现的事实,原书给了浏览器的真实报错文本1。
- 「薄语法、不改行为」的三不原则——原文明写的指导原则5,由第 2 节的对照产物支撑; 同时作者诚实地列出了例外(enum/namespace/参数属性)6。
- 「Vite 不查类型」——产品事实陈述,并扩大到 Node/Bun 的原生支持1213。
- 「noEmit 让 tsc 变成 CI 守门员」——工程建议,逻辑链完整(只查不发 → 不干扰其他工具 → 可进 CI)1415。
原书没说的(我们补):
- 「转译」(transpile)与「编译」(compile)的细微差别——补充(不在书里,来自通用知识): 转译专指「翻译成同级别的另一种语言/版本的源语言」(TS→JS、ES2022→ES5), 编译则更宽泛;日常说「编译 TypeScript」也没错,原书用的是 transpile。
- 2026 年时点 Vite/Node/Bun 的版本细节会变——时效警告:这一章的工具生态部分以书里写作时(2025–2026)为准,用的时候核对你那版的文档。
8. 边界与局限¶
- 工具链部分时效性强:Vite、Bun、Node 的行为在快速演进,「原生支持 TypeScript」的具体范围以当下文档为准; 但「转译 ≠ 类型检查」这个结构性区分不会过时。
- 本章只给了 tsconfig 的惊鸿一瞥(target、strict 等几个默认项),完整配置是第 15 章。
- 「类型对 ≠ 值对」只点了一下(原则的第二「不」),运行时数据校验的整个话题本书不展开。
9. 可带走的¶
- 浏览器/Node 只认 JavaScript;
: string这类记号在运行时不存在,会被 tsc 摘掉。 - tsc 是翻译官:
tsc --init建配置,tsc翻译,tsc --watch常驻翻译兼全项目质检。 - 翻译产物和你的源码几乎逐字相同——TS 是薄语法层,不改运行行为。
- enum、namespace、参数属性是原则的例外(第 10 章审问)。
.gitignore忽略*.js是在宣布:.ts是单一事实源。- Vite / Node / Bun「支持 TypeScript」= 只转译、不查类型——它们不拦错误进生产。
noEmit: true让 tsc 退化成 linter,专职检查,是 CI 的门禁。- 记牢分工:Vite 管「能跑」,
tsc --noEmit管「类型对」。
10. 原文地图¶
| 主题 | 原书位置 | 原文位置 |
|---|---|---|
浏览器的 Unexpected token ':' 报错 |
The Problem with TypeScript in the Browser | text/08-fm-the-problem-with-typescript-in-the-browser.txt:38(搜「Unexpected token」) |
| 删掉类型后「隐式 any」报错 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:50(搜「implicitly has an any」) |
| tsc 负责 TS→JS | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:54(搜「turning TypeScript to JavaScript」) |
| tsconfig 默认项 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:72(搜「compilerOptions」) |
翻译产物对照("use strict") |
同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:91(搜「example.js file」) |
| 「薄语法」三不原则 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:123(搜「thin layer of syntax」) |
| 原则的例外(enum 等) | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:127(搜「exceptions to this guiding principle」) |
| .gitignore 与单一事实源 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:131(搜「source of truth」) |
| --watch 常驻重译 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:137(搜「recompiles」) |
| watch 模式报错的例子 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:156(搜「not assignable」) |
| watch 检查全项目 vs IDE 只看打开文件 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:164(搜「currently open」) |
| Vite 与 HMR | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:170(搜「hot module replacement」) |
| Vite 不查类型的警告 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:176(搜「without type checking」) |
| Node/Bun 原生支持的注 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:176(搜「Node and Bun」) |
| noEmit 与 linter 形态 | 同上 | text/08-fm-the-problem-with-typescript-in-the-browser.txt:182(搜「act more like a linter」) |
-
出处:「The Problem with TypeScript in the Browser」第 38 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:38,搜「Unexpected token」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 50 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:50,搜「implicitly has an any」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 54 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:54,搜「turning TypeScript to JavaScript」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 91–99 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:91,搜「example.js file」)。对照「几乎没变」的原文判断见第 121 段(搜「identical」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 123 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:123,搜「thin layer of syntax」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 127 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:127,搜「exceptions to this guiding principle」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 131 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:131,搜「source of truth」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 137 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:137,搜「recompiles」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 156 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:156,搜「not assignable」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 164 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:164,搜「currently open」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 170 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:170,搜「hot module replacement」)。 ↩ -
出处:「The Problem with TypeScript in the Browser」第 176 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:176,搜「without type checking」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 176 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:176,搜「Node and Bun」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 180 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:180,搜「noEmit」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 182 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:182,搜「act more like a linter」)。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 63–80 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:66,搜「several useful starter configuration options」)。六项默认值见第 70–80 段(搜「es2016」);「many other commented-out options」见第 66 段。 ↩↩ -
出处:「The Problem with TypeScript in the Browser」第 142–162 段(
text/08-fm-the-problem-with-typescript-in-the-browser.txt:160,搜「Watching for file changes」)。终端报错与 VS Code 红线同源见第 144 段(搜「red squiggly line」)。 ↩