
用简单熟悉的场景和代码示例,带你理解 TS 编译器如何将带类型的代码变成 JS,以及为什么能发现类型错误。全文不用复杂术语,看完你就能理解"为什么我写错类型会报错?"
一、编译流程:你的代码经历了什么?
想象你写了一个 TS 文件 app.ts
,执行 tsc
时会发生:
1. 拆解代码(解析阶段)
-
扫描器像读小说一样逐字阅读代码
typescript// 原始代码:let age: number = 18 ↓ 拆解成 token 流 [ "let", "age", ":", "number", "=", "18" ]
-
解析器把这些 token 拼成代码结构树(AST)
typescript// 解析后的 AST 结构(简化版) { type: "VariableDeclaration", declarations: [ { type: "VariableDeclarator", id: { type: "Identifier", name: "age", typeAnnotation: { type: "NumberKeyword" } }, init: { type: "NumericLiteral", value: 18 } } ], kind: "let" }
AST 就像代码的"骨架":
- 保留了代码的结构信息(变量声明、类型标注、初始值等)
- 记录了类型信息(number 类型标注)
- 方便后续的类型检查和代码生成
2. 记住类型(绑定阶段)
编译器会拿个小本本记录每个变量的类型:
typescript
let age: number = "18" // 小本本记下:age 是 number 类型
// ↓ 但发现赋了字符串,立刻标红报错!
// 报错信息:
// Type 'string' is not assignable to type 'number'.
3. 类型大检查(核心阶段)
这是写 TS 时最常打交道的阶段,一起来看看编译器是如何进行类型检查的:
typescript
// 1. 函数定义时的类型检查
function sum(a: number, b: number): number {
return a + b
}
// 2. 函数调用时的类型检查
sum("1", 2) // 检查器发现参数类型不匹配,立刻报错!
编译器会遍历 AST,进行多层检查:
-
参数类型检查
typescript// AST 中的参数类型信息 { type: "FunctionDeclaration", params: [ { type: "Identifier", name: "a", typeAnnotation: { type: "NumberKeyword" } }, { type: "Identifier", name: "b", typeAnnotation: { type: "NumberKeyword" } } ] }
-
调用时的类型匹配
typescript// 调用时的参数类型 sum("1", 2) // 编译器发现: // - "1" 是 string 类型,不匹配 number // - 2 是 number 类型,匹配成功
-
返回值类型检查
typescriptfunction greet(name: string): string { return 123 // 错误:返回值类型不匹配 }
-
对象属性检查
typescriptinterface User { name: string age: number } const user: User = { name: "Tom", age: "18", // 错误:age 应该是 number }
-
类型兼容性检查
typescriptlet x: number = 100 let y: string = x // 错误:number 不能赋值给 string // 但反过来可以 let a: any = "hello" let b: string = a // 正确:any 可以赋值给任何类型
编译器会:
- 遍历 AST 中的每个节点
- 检查类型标注和实际值的匹配
- 验证类型之间的兼容性
- 收集所有类型错误
- 生成详细的错误报告
这就是为什么你能在编写代码时就发现类型错误,而不是等到运行时才发现问题。
4. 擦除类型生成 JS(发射阶段)
这是编译的最后阶段,编译器会:
- 删除所有类型信息
- 转换 TS 特有语法为 JS
- 生成最终的 JS 文件
让我们看看不同类型是如何被擦除的:
typescript
// 1. 接口和类型别名
interface User {
name: string
age: number
}
type Point = { x: number; y: number }
// ↓ 擦除后(完全消失)
// 接口和类型别名在运行时不存在
typescript
// 2. 类型标注
let name: string = "Tom";
const age: number = 18;
function greet(name: string): string { ... }
// ↓ 擦除后
let name = "Tom";
const age = 18;
function greet(name) { ... }
typescript
// 3. 泛型
function identity<T>(arg: T): T {
return arg
}
let result = identity<string>("hello")
// ↓ 擦除后
function identity(arg) {
return arg
}
let result = identity("hello")
typescript
// 4. 类型断言
let someValue: any = "this is a string"
let strLength: number = (someValue as string).length
// ↓ 擦除后
let someValue = "this is a string"
let strLength = someValue.length
typescript
// 5. 枚举
enum Color {
Red,
Green,
Blue,
}
let c: Color = Color.Green
// ↓ 擦除后(转换为对象)
var Color
;(function (Color) {
Color[(Color["Red"] = 0)] = "Red"
Color[(Color["Green"] = 1)] = "Green"
Color[(Color["Blue"] = 2)] = "Blue"
})(Color || (Color = {}))
var c = Color.Green
编译器在擦除类型时会:
- 保留所有运行时需要的代码
- 删除所有类型相关的信息
- 转换 TS 特有语法为等效的 JS 代码
- 确保生成的 JS 代码能正常运行
这就是为什么:
- TS 代码最终会变成普通的 JS
- 类型错误不会影响运行时
- 你可以在 JS 项目中逐步引入 TS
二、为什么能发现类型错误?(类型系统揭秘)
1. 你的类型标注被编译器记住了
- 显式标注 :你写的
: string
会被编译器存在符号表里
typescript
let price: number
price = "100" // 符号表发现类型不符 → 报错
- 自动推断:没写类型时编译器会猜
typescript
let isAdmin = true
isAdmin = 1 // 推断 isAdmin 是 boolean → 报错
2. 遇到复杂类型也不怕
- 泛型检查:像函数参数一样检查类型参数
typescript
function identity<T>(arg: T): T {
return arg
}
identity<string>(100) // 检查 T 是否是 string → 报错
- 接口校验:对象形状必须匹配
typescript
interface Cat {
name: string
}
const cat: Cat = { name: "Tom", age: 2 } // 发现多余属性 → 报错
三、早该知道的编译器秘密
1. 加速编译的技巧
- 关掉严格模式:
strict: false
(新手友好,但会减少类型检查) - 跳过库检查:
skipLibCheck: true
(减少 node_modules 检查) - 增量编译:
tsc --incremental
(只编译改动的文件)
2. 看懂报错信息
遇到错误时关注三个信息:
typescript
// 错误代码位置
app.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.
(12,5)
:第 12 行第 5 列TS2322
:错误编号(查文档用)- 错误描述:类型不匹配的具体原因
四、实战:从错误理解编译器
让我们通过一个完整的例子,看看编译器是如何一步步发现类型错误的:
typescript
// 1. 首先,我们定义了一个函数
function greet(name: string) {
return `Hello ${name}`
}
// 2. 然后,我们错误地传入了数字
greet(123) // 报错:Argument of type 'number' is not assignable to 'string'
编译器的处理流程
-
词法分析阶段
typescript// 源代码被分解成 token ;[ "function", "greet", "(", "name", ":", "string", ")", "{", "return", "`Hello ${name}`", "}", "greet", "(", "123", ")", ]
-
语法分析阶段
typescript// 生成 AST { type: "Program", body: [ { type: "FunctionDeclaration", id: { type: "Identifier", name: "greet" }, params: [ { type: "Identifier", name: "name", typeAnnotation: { type: "StringKeyword" } } ], body: { /* ... */ } }, { type: "CallExpression", callee: { type: "Identifier", name: "greet" }, arguments: [ { type: "NumericLiteral", value: 123 } ] } ] }
-
类型检查阶段
typescript// 编译器发现: // 1. greet 函数的参数 name 被声明为 string 类型 // 2. 调用时传入的是数字 123 // 3. number 类型不能赋值给 string 类型 // 4. 收集错误信息 { errorCode: "TS2345", message: "Argument of type 'number' is not assignable to parameter of type 'string'", location: { line: 4, column: 6 } }
-
错误报告阶段
typescript// 编译器生成友好的错误信息 app.ts(4,6): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
如何修复这个错误?
-
正确的类型转换
typescriptgreet(123.toString()) // 将数字转换为字符串
-
修改函数定义
typescriptfunction greet(name: string | number) { return `Hello ${name}` }
-
使用类型断言(不推荐)
typescriptgreet(123 as any) // 绕过类型检查
编译器的智能提示
当你输入代码时,IDE 会基于类型信息提供智能提示:
typescript
const user = {
name: "Tom",
age: 18
}
user. // IDE 会提示可用的属性:name, age
这就是为什么 TypeScript 能帮助你:
- 在编写代码时就发现错误
- 提供准确的代码补全
- 重构代码时保持类型安全

总结
TypeScript 编译器通过四个主要阶段将 TS 代码转换为 JS:
- 解析阶段:将代码拆解为 token 并构建 AST
- 绑定阶段:记录和存储类型信息
- 类型检查阶段:验证类型正确性
- 发射阶段:擦除类型信息,生成 JS 代码
下次遇到类型错误时,想想编译器在背后帮你做的这些检查工作,是不是觉得报错也没那么讨厌了?