编程语言研究书库

开发流水线里的 TypeScript — tsc、转译与「类型只在写的时候存在」

开发流水线里的 TypeScript — tsc、转译与「类型只在写的时候存在」

这一章讲一件事: 你的 .ts 文件怎么变成浏览器能跑的东西,以及这件事背后那条全书级的指导原则。 读完你会彻底明白「类型在运行时去哪了」——以及为什么这既是 TypeScript 最大的优点,也是它一切局限的根。

1. 先看现象:一个文件,两种死法(又一次)

主线例子还是上一章那个 run 函数,这次看它在一个具体的陷阱里怎么死两次。

你有一个 example.ts:

const run = (message: string) => {
  console.log(message);
};

run("Hello!");

和一个引用它的 index.html(<script src="example.ts"></script>)。 在浏览器里打开页面,控制台报错:

Uncaught SyntaxError: Unexpected token ':'(意外的记号:冒号)

为什么? 因为浏览器读不了 TypeScript——(message: string) 里那个冒号和 string, 是 TypeScript 的语法,不是 JavaScript 的语法1。

好,那把冒号删掉,变成纯 JavaScript?TypeScript 这关又过不去: 删掉 : string 后,编辑器在 message 下面画红线,报错说它「隐式地是 any 类型」2。

这就是本章的入口矛盾,也是整个 TypeScript 工作流的原点:

  带类型(: string)  → 浏览器死:不认识冒号
  不带类型          → tsc 死:参数没有类型,等于对编译器隐身

  图说:同一个文件,两套语法规则各自执法。出路只有一条——翻译。

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 步的产物——这是本章最重要的一段对照:

// 你写的 example.ts
const run = (message: string) => {
  console.log(message);
};
run("Hello!");
// 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 二选一 就是来修这个默认值的)5。 第二,--init 生成的文件里还有大量被注释掉的备选——它们不是配置,是 官方塞进来的「选项菜单」,用到再解开5。

3. 指导原则:一层薄语法,且不改你的代码

由上面的对照,原书立起一条全书级的指导原则,值得整段记住:

TypeScript 是 JavaScript 之上的一层薄语法,但它不改变代码的工作方式。 它不加运行时校验,不优化性能。它只是加上类型、改善开发体验, 然后在编译时把类型摘掉。6

把这条原则的三个「不」拆开看,每一个都是后文的伏笔:

原则说「不」含义伏笔
不改运行行为跑起来的代码和你写的逻辑一模一样类型对运行时不可见(第 11 章的「撒谎账单」)
不加运行时校验类型对了不代表值真的对网络数据、JSON 进来时类型帮不上忙
不优化性能类型不是给机器加速用的呼应第 01 章:类型的受益者是工具和人,不是运行时

原则有例外,书里也明说了:枚举(enum)、命名空间(namespace)、类参数属性(class parameter properties) 这三个特性会生成普通 JavaScript 里不存在的东西——它们会改变运行代码,所以被单独立为「只有 TypeScript 有的特性」在第 10 章审问7。

版本控制的一个约定:既然 .js 是 .ts 的翻译产物,就该在 .gitignore 里忽略 *.js—— 这不只是卫生问题,更是在向所有用仓库的人宣布:.ts 才是源码的单一事实源8。

4. watch 模式:tsc 当常驻质检员

每次都手动跑 tsc,改两下就烦了,而且忘了跑就纳闷「浏览器里怎么没变」。 tsc --watch 让 tsc 常驻:你一保存,它立刻重新翻译9。

tsc 在终端报的错,和 IDE 里的红线是同一份检查的两个出口: 比如你把 message: string 改成 message: number,watch 模式下的终端立刻显示 Argument of type 'string' is not assignable to parameter of type 'number', 改回来,报错消失、.js 自动更新10。

原书指出 watch 模式的一个独特价值:它检查整个项目,而 IDE 只显示当前打开文件的错误11。 项目一大,这个差别就是「漏报」和「不漏报」的差别。

把终端里的完整报错剧本走一遍,这个「常驻质检员」的工作方式就具体了。 你故意把 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 里那份「出错的文件被画上红线」的提示,和终端里的报错是同一次检查、 两个出口——不存在「终端认了、编辑器不认」的两份真相12。

5. 现代框架时代:Vite 能转译,但不查类型

真实的前端项目不会只用一个 tsc——还要处理 CSS、压缩、打包。 以 Vite 为代表的现代工具链把这些全包了,顺带也把 .ts 翻成 .js, 还提供热模块替换(HMR):改代码不用刷新页面,浏览器里立刻变13。

但这里有一个全书最要紧的现代陷阱,原书是敲着黑板讲的:

Vite 这类工具「支持 TypeScript」的意思是只做转译、不做类型检查。 你的代码里有类型错误,Vite 照样高高兴兴跑开发服务器,一个字都不提醒; 它甚至不会拦着你把错误带进生产环境——因为它根本不知道有错。14

书里补的注把这个坑又挖深了一层:Node、Bun 这些运行时现在也「原生支持 TypeScript」了, 但性质一样——只转译、不查类型15。

所以结论很硬:只要你用了任何「原生支持 TypeScript」的工具,就仍然需要 tsc 做类型检查。 「能跑」和「类型对」是两份独立的报告,前者由 Vite/Node 给,后者只能 tsc 给。

6. noEmit:tsc 的第二种形态——只查不发的 linter

既然 Vite 负责产出 .js,tsc 再产一份就是重复劳动。配置里的 noEmit: true 告诉 tsc:「只检查,不产出文件」16。这时 tsc 的角色从翻译官变成质检员—— 一个挂在 TypeScript 身上的 linter(代码检查器)。

这个形态恰好是 CI/CD 流水线想要的:tsc 这一步放进持续集成, 带着类型错误的合并请求就会被拦下17。「类型检查」和「产出代码」就此解耦: 转译归 Vite,检查归 tsc --noEmit,各管一段。

  现代前端流水线的分工:
    Vite  —— 开发服务器、HMR、转译、打包      (只管「能跑」)
    tsc --noEmit —— 全项目类型检查           (只管「类型对」)
    CI    —— 跑 tsc,有错就拦下合并           (把检查变成门禁)

  图说:本章开头那个「两套语法各自执法」的矛盾,到这里被拆成了两条独立流水线。

7. 作者的判断与证据(分开标)

书里有据的:

  • 「浏览器只认 JS、冒号是非法记号」——可当场复现的事实,原书给了浏览器的真实报错文本1。
  • 「薄语法、不改行为」的三不原则——原文明写的指导原则6,由第 2 节的对照产物支撑; 同时作者诚实地列出了例外(enum/namespace/参数属性)7。
  • 「Vite 不查类型」——产品事实陈述,并扩大到 Node/Bun 的原生支持1415。
  • 「noEmit 让 tsc 变成 CI 守门员」——工程建议,逻辑链完整(只查不发 → 不干扰其他工具 → 可进 CI)1617。

原书没说的(我们补):

  • 「转译」(transpile)与「编译」(compile)的细微差别——补充(不在书里,来自通用知识): 转译专指「翻译成同级别的另一种语言/版本的源语言」(TS→JS、ES2022→ES5), 编译则更宽泛;日常说「编译 TypeScript」也没错,原书用的是 transpile。
  • 2026 年时点 Vite/Node/Bun 的版本细节会变——时效警告:这一章的工具生态部分以书里写作时(2025–2026)为准,用的时候核对你那版的文档。

8. 边界与局限

  • 工具链部分时效性强:Vite、Bun、Node 的行为在快速演进,「原生支持 TypeScript」的具体范围以当下文档为准; 但「转译 ≠ 类型检查」这个结构性区分不会过时。
  • 本章只给了 tsconfig 的惊鸿一瞥(target、strict 等几个默认项),完整配置是第 15 章。
  • 「类型对 ≠ 值对」只点了一下(原则的第二「不」),运行时数据校验的整个话题本书不展开。

9. 可带走的

  1. 浏览器/Node 只认 JavaScript;: string 这类记号在运行时不存在,会被 tsc 摘掉。
  2. tsc 是翻译官:tsc --init 建配置,tsc 翻译,tsc --watch 常驻翻译兼全项目质检。
  3. 翻译产物和你的源码几乎逐字相同——TS 是薄语法层,不改运行行为。
  4. enum、namespace、参数属性是原则的例外(第 10 章审问)。
  5. .gitignore 忽略 *.js 是在宣布:.ts 是单一事实源。
  6. Vite / Node / Bun「支持 TypeScript」= 只转译、不查类型——它们不拦错误进生产。
  7. noEmit: true 让 tsc 退化成 linter,专职检查,是 CI 的门禁。
  8. 记牢分工:Vite 管「能跑」,tsc --noEmit 管「类型对」。

10. 原文地图

主题原书位置原文位置
浏览器的 Unexpected token ':' 报错The Problem with TypeScript in the Browsertext/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」)

Footnotes

  1. 出处:「The Problem with TypeScript in the Browser」第 38 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:38,搜「Unexpected token」)。 ↩ ↩2

  2. 出处:「The Problem with TypeScript in the Browser」第 50 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:50,搜「implicitly has an any」)。 ↩

  3. 出处:「The Problem with TypeScript in the Browser」第 54 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:54,搜「turning TypeScript to JavaScript」)。 ↩

  4. 出处:「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」)。 ↩

  5. 出处:「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 段。 ↩ ↩2

  6. 出处:「The Problem with TypeScript in the Browser」第 123 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:123,搜「thin layer of syntax」)。 ↩ ↩2

  7. 出处:「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」)。 ↩ ↩2

  8. 出处:「The Problem with TypeScript in the Browser」第 131 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:131,搜「source of truth」)。 ↩

  9. 出处:「The Problem with TypeScript in the Browser」第 137 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:137,搜「recompiles」)。 ↩

  10. 出处:「The Problem with TypeScript in the Browser」第 156 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:156,搜「not assignable」)。 ↩

  11. 出处:「The Problem with TypeScript in the Browser」第 164 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:164,搜「currently open」)。 ↩

  12. 出处:「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」)。 ↩

  13. 出处:「The Problem with TypeScript in the Browser」第 170 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:170,搜「hot module replacement」)。 ↩

  14. 出处:「The Problem with TypeScript in the Browser」第 176 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:176,搜「without type checking」)。 ↩ ↩2

  15. 出处:「The Problem with TypeScript in the Browser」第 176 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:176,搜「Node and Bun」)。 ↩ ↩2

  16. 出处:「The Problem with TypeScript in the Browser」第 180 段(text/08-fm-the-problem-with-typescript-in-the-browser.txt:180,搜「noEmit」)。 ↩ ↩2

  17. 出处:「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」)。 ↩ ↩2

On this page