TypeScript工作流深度解析:从.ts到.js发生了什么?
- TypeScript编译全景图
- tsc编译全过程解析
-
- 阶段一:解析阶段(Parsing)
-
- 步骤1:词法分析(Lexical Analysis)
- 步骤2:语法分析(Syntax Analysis)
- 阶段二:语义分析与类型检查
-
- 步骤3:创建符号表(Symbol Table)
- 步骤4:类型检查(Type Checking)
- 阶段三:代码转换与生成
-
- 步骤5:类型擦除(Type Erasure)
- 步骤6:降级转换(Downleveling)
- 步骤7:代码生成与输出
- 类型检查 vs 代码生成的关系
-
- 分离但协作的两个系统
- 关键特性:渐进式检查
- 为什么有些类型错误不影响运行?
-
- 案例一:类型系统无法捕获的运行时错误
- 案例二:类型断言绕过检查
- 案例三:外部JavaScript库
- 结语
当我们在IDE中写下
const name: string = 'TypeScript'时,这行优雅的类型注解最终如何变成浏览器能理解的 JavaScript ?本篇文章将深入TypeScript编译器的核心,揭开从.ts到.js的神秘面纱。
TypeScript编译全景图
首先,让我们通过一张完整的流程图来理解TypeScript编译的全过程:

tsc编译全过程解析
阶段一:解析阶段(Parsing)
typescript
// 输入:TypeScript源代码
const calculate = (x: number, y: number): number => x + y;
步骤1:词法分析(Lexical Analysis)
编译器首先将源代码字符串拆分成一个个词法单元(tokens):
const(关键字)calculate(标识符)=(运算符)((分隔符)x(标识符):(分隔符)number(类型关键字)- ...等等
步骤2:语法分析(Syntax Analysis)
根据TypeScript语法规则,将tokens组合成抽象语法树(AST):
json
{
"type": "VariableDeclaration",
"declarations": [{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "calculate"
},
"init": {
"type": "ArrowFunctionExpression",
"params": [
{
"type": "Identifier",
"name": "x",
"typeAnnotation": {
"type": "TypeAnnotation",
"typeAnnotation": { "type": "NumberType" }
}
},
// ... 类似结构
],
"returnType": {
"type": "TypeAnnotation",
"typeAnnotation": { "type": "NumberType" }
},
"body": { /* ... */ }
}
}]
}
阶段二:语义分析与类型检查
这是TypeScript与纯JavaScript编译器(如Babel)的核心区别所在。
步骤3:创建符号表(Symbol Table)
编译器遍历AST,收集所有标识符的信息:
calculate:函数,接收两个number参数,返回numberx:参数,类型为numbery:参数,类型为number
步骤4:类型检查(Type Checking)
基于符号表进行类型推断和验证:
typescript
// 示例1:正确的代码
const a: number = 5;
const b: number = 10;
const result = a + b; // ✅ 类型检查通过
// 示例2:错误的代码
const str: string = "hello";
const num: number = 5;
const error = str + num;
// ⚠️ TypeScript会警告:虽然能运行,但可能是逻辑错误
阶段三:代码转换与生成
步骤5:类型擦除(Type Erasure)
这是TypeScript设计哲学的关键体现------所有类型信息在运行时都不存在:
typescript
// 编译前 (.ts)
interface User {
id: number;
name: string;
age?: number;
}
function greet(user: User): string {
return `Hello, ${user.name}!`;
}
// 编译后 (.js)
function greet(user) {
return "Hello, " + user.name + "!";
}
// 注意:User接口完全消失了!
步骤6:降级转换(Downleveling)
根据tsconfig.json中的target配置,将现代JavaScript语法转换为目标版本:
typescript
// 编译前(ES2022)
class User {
#privateField = "secret"; // 私有字段
async fetchData() {
const response = await fetch('/api');
return response.json();
}
}
// 编译为ES5(target: "es5")
var User = /** @class */ (function () {
function User() {
_privateField.set(this, "secret");
}
User.prototype.fetchData = function () {
return __awaiter(this, void 0, void 0, function () {
var response;
return __generator(this, function (_a) {
// 复杂的转译代码...
});
});
};
return User;
}());
var _privateField = new WeakMap();
步骤7:代码生成与输出
bash
# tsc的完整工作流程
输入: src/
├── index.ts # TypeScript源码
├── utils.ts
└── types.ts
处理: tsc编译器
├── 解析所有文件
├── 构建项目引用图
├── 类型检查
├── 转换代码
└── 生成输出
输出: dist/
├── index.js # JavaScript代码
├── utils.js
├── index.d.ts # 类型声明文件(可选)
└── index.js.map # Source Map(可选)
类型检查 vs 代码生成的关系
分离但协作的两个系统
TypeScript编译器内部实际上有两个相对独立的子系统:
typescript
// 概念模型
class TypeScriptCompiler {
// 类型检查器
private typeChecker: TypeChecker;
// 代码生成器(基于JavaScript编译器)
private emitter: Emitter;
compile(sourceFile: SourceFile): OutputFile[] {
// 1. 类型检查(可能失败,但不影响继续)
const diagnostics = this.typeChecker.check(sourceFile);
// 2. 报告错误(但不停止)
this.reportDiagnostics(diagnostics);
// 3. 代码生成(无论是否有类型错误)
const jsCode = this.emitter.emit(sourceFile);
return [jsCode];
}
}
关键特性:渐进式检查
TypeScript采用渐进式类型检查策略:
typescript
// 文件A.ts - 先编译这个
export function add(a: number, b: number): number {
return a + b;
}
// 文件B.ts - 后编译,依赖于A.ts
import { add } from './A';
// 即使A.ts有类型错误,B.ts仍然可以:
// 1. 获得A.ts的类型信息(可能不完整)
// 2. 检查自身代码的类型正确性
// 3. 生成JavaScript代码
const result = add(5, "10"); // ❌ 这里会报错
为什么有些类型错误不影响运行?
案例一:类型系统无法捕获的运行时错误
typescript
// TypeScript编译时检查通过 ✅
function divide(a: number, b: number): number {
return a / b;
}
// 运行时可能出错 ❌
const result = divide(10, 0); // Infinity,可能不是期望的结果
const element = document.getElementById("nonexistent");
element.innerHTML = "hello"; // Runtime Error: Cannot read property...
案例二:类型断言绕过检查
typescript
interface SafeData {
value: string;
}
// 编译时:欺骗TypeScript
const unsafeData = JSON.parse('{"value": 123}') as SafeData;
// 运行时:没有问题...暂时
console.log(unsafeData.value); // 123 (number, 不是string!)
// 直到这里才可能出错
const length = unsafeData.value.length; // Runtime Error!
案例三:外部JavaScript库
typescript
// 假设使用了一个没有类型定义的第三方库
declare const legacyLib: any; // 使用any类型
const result = legacyLib.calculate(1, 2, 3);
// TypeScript: ✅ 没有类型错误(因为any)
// 运行时: 可能成功,也可能崩溃
结语
本文介绍了TypeScript的核心工作流程,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!