TypeScript 编译与执行

TypeScript 代码不能直接在任何运行时中执行——它必须先被编译(更精确地说,转译)成 JavaScript。

习惯了 python script.py 写完即跑之后,TS 的”写完 → 类型检查 → 构建 → 运行”链路是多出来的一个台阶。但这不是 TS 的缺陷——你在 Python 里靠 mypy 做检查的那种”安全意识”,在 TS 里被 tsc 编译期强制内建了。Node 23 的 strip-types 正在让这个台阶变矮。


类型擦除:TS 编译的核心

什么是类型擦除

TypeScript 的类型系统是纯编译期的。编译器验证类型后,将所有类型信息删除,输出纯 JavaScript。运行时的代码不知道任何类型的存在。

// 编译前 (TypeScript)
interface User {
  id: string;
  name: string;
  email?: string;
}
 
function createUser(data: User): User {
  const user = { ...data, id: crypto.randomUUID() };
  console.log(typeof user.email); // 运行时:string | undefined
  return user;
}
// 编译后 (JavaScript) — 类型全部消失
function createUser(data) {
  const user = { ...data, id: crypto.randomUUID() };
  console.log(typeof user.email);
  return user;
}

关键后果:类型信息在运行时不可用。如果你在 TS 中依赖类型做运行时判断,代码会以预期之外的方式运行——因为那些类型标注在编译后根本不存在。

// 误区:想用泛型类型做 instanceof 检查
function isUser<T>(obj: T): boolean {
  return obj instanceof User; // 编译错误!User 在运行时不存在
}
 
// 正确:用 runtime schema 库(Zod/Valibot)做校验
import { z } from "zod";
const UserSchema = z.object({ id: z.string(), name: z.string() });
function isUser(obj: unknown): obj is z.infer<typeof UserSchema> {
  return UserSchema.safeParse(obj).success;
}

与 Python 的关键区别

维度TypeScriptPython
类型存在时机编译期(编译后完全消失)运行时(注解保留在 __annotations__
类型影响执行❌ 零运行时影响❌ 默认不检查,但注解可被库读取
运行时校验必须用 Zod/Valibot可选用 Pydantic
编译需求必须编译(类型擦除)不需要(解释器忽略注解)

Python 经验者最易踩的坑:你习惯了 Pydantic BaseModel 在运行时自动校验类型——user = UserSchema(name=123) 会当场抛 ValidationError。TS 中没有这种机制——类型标注只影响 tsc 的检查,运行时完全消失。必须显式调用 Zod 的 .parse() 才能在运行时捕获错误。


tsconfig.json:编译器的控制面板

核心字段速查

{
  "compilerOptions": {
    // ── 目标与模块 ──
    "target": "ES2022",           // 编译到哪个 JS 版本
    "module": "ESNext",           // 使用哪种模块语法(ESM / CJS)
    "moduleResolution": "bundler", // 模块解析策略
    "lib": ["ES2022", "DOM"],     // 可用的类型声明(内置 API)
 
    // ── 严格性 ──
    "strict": true,               // 开启所有严格检查(推荐)
    "noUncheckedIndexedAccess": true, // 索引访问包含 undefined
    "exactOptionalPropertyTypes": true, // ? 可选 ≠ 可赋值 undefined
 
    // ── 路径与输出 ──
    "outDir": "dist",             // 编译输出目录
    "rootDir": "src",             // 源码根目录
    "paths": {                    // 路径别名
      "@/*": ["./src/*"]
    },
    "baseUrl": ".",
 
    // ── JS 互操作 ──
    "allowJs": true,              // 允许编译 .js 文件
    "esModuleInterop": true,      // 兼容 CJS 默认导出
 
    // ── 其他 ──
    "declaration": true,          // 生成 .d.ts 声明文件
    "declarationMap": true,       // 生成声明的 source map
    "sourceMap": true,            // 生成 source map
    "skipLibCheck": true,         // 跳过 .d.ts 类型检查(加速)
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

moduleResolution:2026 年的选择

这是 tsconfig最重要也最容易配错的字段,决定了 TS 如何找到你的 import。

策略适用场景说明
bundlerVite/Next.js 项目2026 年推荐,模拟打包器的解析方式
node16 / nodenextNode 原生 ESM 项目严格按 Node ESM 规则解析
node (legacy)纯 Node CJS 项目旧策略,不推荐新项目使用
classic几乎不用最早的策略,已废弃
// `bundler` 策略的行为
import "./foo";        // 自动尝试 .ts/.tsx/.js/.jsx
import "./foo.js";     // 也接受 .js 扩展名(Vite 风格)
 
// `node16` 策略的行为
import "./foo.js";     // 必须写 .js 扩展名(Node ESM 规范要求)

target:编译到哪个 JS 版本

target说明
ES2022现代 Node 22+ / 现代浏览器,推荐
ES2020兼容 Node 14+ / 稍老浏览器
ES2015广泛兼容(会生成大量 polyfill 风格的降级代码)

选择原则:不为了”最大兼容”而降级到 ES2015。现代工具链(Vite/esbuild)会自动处理不同浏览器的兼容性,tsconfigtarget 主要决定 async/await 等语言特性的降级行为。


构建管线:2026 年的分层模型

两条独立管线

graph LR
    subgraph 类型检查管线
        SRC[src/**/*.ts] --> TSC["tsc --noEmit<br/>(只做类型检查,不输出代码)"]
    end

    subgraph 构建管线
        SRC2[src/**/*.ts] --> ESBUILD[esbuild/swc<br/>(类型擦除 + 转译)]
        ESBUILD --> ROLLUP[Rollup<br/>(打包 + tree-shaking + 优化)]
        ROLLUP --> DIST[dist/**/*.js]
    end

三大转译器对比

维度tscesbuildswc
实现语言TypeScript (自举)GoRust
速度★★★★★★★(快 50–100×)★★★★★(快 50–100×)
类型检查✅ 完整❌ 不检查❌ 不检查
降级能力✅ 完整✅ 覆盖主流场景✅ 覆盖主流场景
插件生态⚠️ 有限✅ 广泛✅ 被 Next.js 等采用
典型角色CI 类型检查 (--noEmit)Vite dev 转译Next.js Turbopack

生产级 package.json scripts

{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview"
  }
}

CI 流程

# .github/workflows/ci.yml
jobs:
  quality:
    steps:
      # 两条管线并行运行
      - run: pnpm typecheck    # 类型检查
      - run: pnpm lint         # 代码质量
      - run: pnpm test         # 测试
      - run: pnpm build        # 构建(不包含 tsc,独立运行)

类型擦除的特殊情况

不能简单 strip 的语法

TS 有些特性在编译时生成实际的 JS 代码,不是简单的类型擦除:

// enum — 生成 IIFE(立即执行函数表达式)
enum Color { Red, Green, Blue }
// → var Color; (function(Color) { ... })(Color || (Color = {}));
 
// namespace — 生成闭包
namespace Utils {
  export function log(msg: string) { console.log(msg); }
}
// → var Utils; (function(Utils) { ... })(Utils || (Utils = {}));
 
// 参数属性 — 生成构造函数赋值
class User {
  constructor(public name: string, private age: number) {}
}
// → class User { constructor(name, age) { this.name = name; this.age = age; } }
 
// 装饰器 — 生成元数据调用(TS 5.0+ ECMAScript Decorators)
class Service {
  @log
  method() {}
}

这些特性在 Node 23 的 --experimental-strip-types 模式下不能直接运行,必须经过完整的 tsc 编译。2026 年建议:新代码中尽量避开这些需要降级的 TS 特有语法。

const enum 和内联优化

// const enum — 编译时内联(零运行时开销)
const enum Direction { Up, Down, Left, Right }
const dir = Direction.Up;
 
// 编译后直接变成:
const dir = 0; // Direction.Up 被内联为 0

const enum 在跨项目引用时可能出问题(如果声明文件丢失),需要权衡使用。


与 Python 编译模型的对比

维度TypeScriptPython
编译步骤必须(tsc/esbuild/Vite)无(.pyc 缓存可选)
产物.js / .mjs.pyc(仅缓存)
类型擦除编译时强制解释器忽略注解
运行时V8/JSC 引擎CPython 解释器
构建复杂度中高(配置 + 多工具链)低(几乎无构建)
AOT/JITAOT 转译 → JIT 执行JIT 执行(CPython)

Python 的开发心智是”写完就能跑”,TS 是”写完 → 类型检查通过 → 构建 → 运行”。这一步编译/构建不是 TS 的缺陷,而是它提供编译期安全保障的代价。Node 23 的 strip-types 正在模糊这条线。


关联