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 的关键区别
| 维度 | TypeScript | Python |
|---|---|---|
| 类型存在时机 | 编译期(编译后完全消失) | 运行时(注解保留在 __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。
| 策略 | 适用场景 | 说明 |
|---|---|---|
bundler | Vite/Next.js 项目 | 2026 年推荐,模拟打包器的解析方式 |
node16 / nodenext | Node 原生 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)会自动处理不同浏览器的兼容性,tsconfig 的 target 主要决定 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
三大转译器对比
| 维度 | tsc | esbuild | swc |
|---|---|---|---|
| 实现语言 | TypeScript (自举) | Go | Rust |
| 速度 | ★★ | ★★★★★(快 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 被内联为 0const enum 在跨项目引用时可能出问题(如果声明文件丢失),需要权衡使用。
与 Python 编译模型的对比
| 维度 | TypeScript | Python |
|---|---|---|
| 编译步骤 | 必须(tsc/esbuild/Vite) | 无(.pyc 缓存可选) |
| 产物 | .js / .mjs | .pyc(仅缓存) |
| 类型擦除 | 编译时强制 | 解释器忽略注解 |
| 运行时 | V8/JSC 引擎 | CPython 解释器 |
| 构建复杂度 | 中高(配置 + 多工具链) | 低(几乎无构建) |
| AOT/JIT | AOT 转译 → JIT 执行 | JIT 执行(CPython) |
Python 的开发心智是”写完就能跑”,TS 是”写完 → 类型检查通过 → 构建 → 运行”。这一步编译/构建不是 TS 的缺陷,而是它提供编译期安全保障的代价。Node 23 的 strip-types 正在模糊这条线。
关联
- meta/编译与执行 — 跨语言编译模型对比
- TS 工程实践 — 工具链全景(Vite/esbuild/Biome/Vitest)
- TS 运行时模型 — Node/Deno/Bun 如何执行 TS
- TS 类型系统 — 类型擦除前的类型检查
- Python 工程实践 — Python 构建对照
- TypeScript 专题 MOC
- Python: 工程实践 — Python 构建对照