TypeScript工作流深度解析:从.ts到.js发生了什么?

47 阅读4分钟

当我们在IDE中写下 const name: string = ‘TypeScript’ 时,这行优雅的类型注解最终如何变成浏览器能理解的 JavaScript ?本篇文章将深入TypeScript编译器的核心,揭开从.ts到.js的神秘面纱。

TypeScript编译全景图

首先,让我们通过一张完整的流程图来理解TypeScript编译的全过程:

ts到js编译图.png

tsc编译全过程解析

阶段一:解析阶段(Parsing)

// 输入:TypeScript源代码
const calculate = (x: number, y: number): number => x + y;

步骤1:词法分析(Lexical Analysis)

编译器首先将源代码字符串拆分成一个个词法单元(tokens):

  • const (关键字)
  • calculate (标识符)
  • = (运算符)
  • ( (分隔符)
  • x (标识符)
  • : (分隔符)
  • number (类型关键字)
  • ...等等

步骤2:语法分析(Syntax Analysis)

根据TypeScript语法规则,将tokens组合成抽象语法树(AST):

{
  "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参数,返回number
  • x:参数,类型为number
  • y:参数,类型为number

步骤4:类型检查(Type Checking)

基于符号表进行类型推断和验证:

// 示例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设计哲学的关键体现——所有类型信息在运行时都不存在:

// 编译前 (.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语法转换为目标版本:

// 编译前(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:代码生成与输出

# 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编译器内部实际上有两个相对独立的子系统:

// 概念模型
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采用渐进式类型检查策略:

// 文件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编译时检查通过 ✅
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...

案例二:类型断言绕过检查

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库

// 假设使用了一个没有类型定义的第三方库
declare const legacyLib: any; // 使用any类型

const result = legacyLib.calculate(1, 2, 3);
// TypeScript: ✅ 没有类型错误(因为any)
// 运行时: 可能成功,也可能崩溃

结语

本文介绍了TypeScript的核心工作流程,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!