看完就懂!用最简单的方式带你了解 TypeScript 编译器原理

用简单熟悉的场景和代码示例,带你理解 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,进行多层检查:

  1. 参数类型检查

    typescript 复制代码
    // AST 中的参数类型信息
    {
      type: "FunctionDeclaration",
      params: [
        { type: "Identifier", name: "a", typeAnnotation: { type: "NumberKeyword" } },
        { type: "Identifier", name: "b", typeAnnotation: { type: "NumberKeyword" } }
      ]
    }
  2. 调用时的类型匹配

    typescript 复制代码
    // 调用时的参数类型
    sum("1", 2)
    // 编译器发现:
    // - "1" 是 string 类型,不匹配 number
    // - 2 是 number 类型,匹配成功
  3. 返回值类型检查

    typescript 复制代码
    function greet(name: string): string {
      return 123 // 错误:返回值类型不匹配
    }
  4. 对象属性检查

    typescript 复制代码
    interface User {
      name: string
      age: number
    }
    
    const user: User = {
      name: "Tom",
      age: "18", // 错误:age 应该是 number
    }
  5. 类型兼容性检查

    typescript 复制代码
    let x: number = 100
    let y: string = x // 错误:number 不能赋值给 string
    
    // 但反过来可以
    let a: any = "hello"
    let b: string = a // 正确:any 可以赋值给任何类型

编译器会:

  • 遍历 AST 中的每个节点
  • 检查类型标注和实际值的匹配
  • 验证类型之间的兼容性
  • 收集所有类型错误
  • 生成详细的错误报告

这就是为什么你能在编写代码时就发现类型错误,而不是等到运行时才发现问题。

4. 擦除类型生成 JS(发射阶段)

这是编译的最后阶段,编译器会:

  1. 删除所有类型信息
  2. 转换 TS 特有语法为 JS
  3. 生成最终的 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 代码能正常运行

这就是为什么:

  1. TS 代码最终会变成普通的 JS
  2. 类型错误不会影响运行时
  3. 你可以在 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'

编译器的处理流程

  1. 词法分析阶段

    typescript 复制代码
    // 源代码被分解成 token
    ;[
      "function",
      "greet",
      "(",
      "name",
      ":",
      "string",
      ")",
      "{",
      "return",
      "`Hello ${name}`",
      "}",
      "greet",
      "(",
      "123",
      ")",
    ]
  2. 语法分析阶段

    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 }
          ]
        }
      ]
    }
  3. 类型检查阶段

    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 }
    }
  4. 错误报告阶段

    typescript 复制代码
    // 编译器生成友好的错误信息
    app.ts(4,6): error TS2345:
    Argument of type 'number' is not assignable to parameter of type 'string'.

如何修复这个错误?

  1. 正确的类型转换

    typescript 复制代码
    greet(123.toString()) // 将数字转换为字符串
  2. 修改函数定义

    typescript 复制代码
    function greet(name: string | number) {
      return `Hello ${name}`
    }
  3. 使用类型断言(不推荐)

    typescript 复制代码
    greet(123 as any) // 绕过类型检查

编译器的智能提示

当你输入代码时,IDE 会基于类型信息提供智能提示:

typescript 复制代码
const user = {
  name: "Tom",
  age: 18
}

user. // IDE 会提示可用的属性:name, age

这就是为什么 TypeScript 能帮助你:

  1. 在编写代码时就发现错误
  2. 提供准确的代码补全
  3. 重构代码时保持类型安全

总结

TypeScript 编译器通过四个主要阶段将 TS 代码转换为 JS:

  1. 解析阶段:将代码拆解为 token 并构建 AST
  2. 绑定阶段:记录和存储类型信息
  3. 类型检查阶段:验证类型正确性
  4. 发射阶段:擦除类型信息,生成 JS 代码

下次遇到类型错误时,想想编译器在背后帮你做的这些检查工作,是不是觉得报错也没那么讨厌了?

相关推荐
毕小宝10 分钟前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML10 分钟前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia31111 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生27 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇41 分钟前
一文搞定CSS Grid布局
前端
0xHashlet1 小时前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝1 小时前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大1 小时前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂1 小时前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端
凌叁儿1 小时前
从零开始搭建Django博客③--前端界面实现
前端·python·django