Chapter 01

TypeScript 起源与类型系统核心

从 JavaScript 的动态类型痛点出发,理解 TypeScript 的结构化类型系统与编译工作流

TypeScript 的诞生

Anders Hejlsberg 与大型 JS 项目的困境

2010 年,微软内部面临一个严峻挑战:团队正在用 JavaScript 开发 Visual Studio 的 Web 版本(后来的 Monaco Editor,即今天 VS Code 的编辑器核心)。这个项目代码量庞大,涉及数十名工程师协作,而 JavaScript 的动态类型特性让这个项目变成了噩梦。

工程师们发现,在大型代码库中,一个函数改了参数名,调用方根本不知道;一个对象新增了必填字段,所有漏掉填写的地方只有在运行时才会暴露;IDE 的代码补全功能因为无法确定类型而形同虚设。

这个项目的负责人正是 Anders Hejlsberg——他同时也是 Turbo Pascal 之父、Delphi 之父和 C# 之父。他决定为 JavaScript 设计一套类型系统,这就是 TypeScript 的起点。

TypeScript 关键时间线

2010 年
Anders Hejlsberg 在微软内部开始 TypeScript 项目,最初代号「Strada」(道路)。
2012 年 10 月
TypeScript 0.8 公开发布,开源于 CodePlex(后迁移到 GitHub)。社区初期反应热烈。
2014 年
TypeScript 1.0 发布;同年 Google Angular 团队宣布 Angular 2 将用 TypeScript 编写,大幅提升了知名度。
2016 年
TypeScript 2.0 引入可空性检查(strictNullChecks),这是 TypeScript 历史上最重要的特性之一。
2018 年
TypeScript 3.0,React 社区大规模迁移,Vue 3 宣布用 TS 重写。TS 成为前端标准配置。
2022-2024 年
TypeScript 4.x 和 5.x 系列发布,引入 satisfies 运算符、const 类型参数、装饰器 Stage 3 支持等重要特性。

今天谁在用 TypeScript?

TypeScript 已成为现代前后端开发的标准语言:

JavaScript 的动态类型痛点

典型的运行时错误场景

以下是纯 JavaScript 中常见的类型相关错误,这些错误只有在运行时才能发现:

// 场景1:函数参数类型错误
function formatPrice(price) {
  return price.toFixed(2) + ' 元';
}

formatPrice('100');  // 运行时错误:String 没有 toFixed 方法
formatPrice(null);  // TypeError: Cannot read property 'toFixed' of null

// 场景2:对象属性拼写错误
const user = { name: '张三', age: 28 };
console.log(user.nname);  // undefined(没有报错!潜在的 bug)

// 场景3:重构后遗留的错误调用
// 原来是 getUserById(id),重构后改为 getUser(userId)
const user2 = getUserById('123');  // 运行时才发现函数不存在
// TypeScript 的处理方式
function formatPrice(price: number): string {
  return price.toFixed(2) + ' 元';
}

formatPrice('100');  // ❌ 编译错误:Argument of type 'string' is not assignable to type 'number'
formatPrice(null);  // ❌ 编译错误:Argument of type 'null' is not assignable to type 'number'

// 对象属性访问的类型安全
interface User {
  name: string;
  age: number;
}
const user: User = { name: '张三', age: 28 };
console.log(user.nname);  // ❌ 编译错误:Property 'nname' does not exist on type 'User'
「左移」错误发现时机

TypeScript 的核心价值在于将错误发现的时机「左移」(Shift Left):从生产环境运行时 → 测试阶段 → 本地开发编译时。错误发现得越早,修复成本越低。研究表明,TypeScript 可以在编译时捕获大约 15% 的 JavaScript bug(来自 Airbnb 内部研究数据)。

TypeScript 编译过程

tsc:TypeScript 编译器

tsc(TypeScript Compiler)是 TypeScript 的官方编译器。它的工作分为两个阶段:

类型检查阶段
分析 .ts 文件中所有的类型标注和推断,检查是否存在类型错误。这是 TypeScript 独有的阶段,纯 JS 没有。
转译阶段(Emit)
将 TypeScript 代码「擦除」所有类型信息,转换为目标 JavaScript(根据 tsconfig 的 target 配置,可以是 ES5/ES2020 等)。
TypeScript 在运行时消失了

TypeScript 的类型只存在于编译时。编译后的 JavaScript 中没有任何类型信息——interface、type alias、类型标注全部被删除。这意味着:
1. TypeScript 不增加任何运行时开销
2. 运行时的类型验证(如 API 响应数据)仍然需要用 Zod/Yup 等库手动实现
3. 说「TypeScript 是 JavaScript」是准确的——所有 TS 代码最终都运行为 JS

编译流程图

你的 TypeScript 代码 │ ▼ ┌─────────────────┐ │ 词法分析/解析 │ 将源码解析为 AST(抽象语法树) └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 类型检查器 │ 遍历 AST,解析类型,报告错误 │ (Type Checker) │ 这是 TypeScript 的核心 └────────┬────────┘ │ 类型检查通过(或 --noEmit 时停止) ▼ ┌─────────────────┐ │ 代码生成器 │ 擦除类型,生成目标 JS 代码 │ (Emitter) │ 降级语法(如箭头函数 → function) └────────┬────────┘ │ ▼ JavaScript 输出 + .d.ts 声明文件(可选) + .js.map 源码映射(可选)

结构化类型系统(Duck Typing)

名义类型 vs 结构化类型

TypeScript 使用结构化类型系统(Structural Type System),而不是 Java/C# 那样的名义类型系统(Nominal Type System)。这是 TypeScript 设计中最重要的决策之一。

名义类型系统
类型兼容性由类型的名字(声明)决定。如果 Java 中 class Dog 和 class Cat 都有 .run() 方法,但它们不相关,不能互换使用。
结构化类型系统
类型兼容性由类型的结构(属性和方法的集合)决定。如果两个类型有相同的「形状」,TypeScript 就认为它们兼容,无论它们的名字是什么。这也叫「鸭子类型」(Duck Typing):如果它走路像鸭子、叫声像鸭子,那它就是鸭子。
// 结构化类型系统示例
interface Point2D {
  x: number;
  y: number;
}

interface Vector2D {
  x: number;
  y: number;
}

// Point2D 和 Vector2D 结构完全相同,TypeScript 认为它们兼容
function printPoint(p: Point2D) {
  console.log(`(${p.x}, ${p.y})`);
}

const vec: Vector2D = { x: 1, y: 2 };
printPoint(vec);  // ✅ OK!结构兼容

// 更强的例子:匿名对象字面量
printPoint({ x: 3, y: 4, z: 5 });  // ✅ OK!额外属性在赋值给变量时 OK
// (注意:直接作为对象字面量传参时会有「额外属性检查」,这是另一个话题)

// 名义类型系统则需要显式实现接口才行
// TypeScript 无需 implements Point2D 就能使用

为什么结构化类型适合 TypeScript?

JavaScript 本身就使用结构化类型(任何有相同属性的对象都能当同一种类型使用),TypeScript 的结构化类型系统完美地与 JS 的编程习惯匹配。如果 TypeScript 使用名义类型,大量现有的 JS 代码模式将无法正确类型化。

安装与环境配置

安装 TypeScript

# 全局安装 tsc(不推荐用于项目)
npm install -g typescript
tsc --version  # Version 5.x.x

# 推荐:项目本地安装(每个项目独立版本)
npm init -y
npm install --save-dev typescript

# 运行本地 tsc
npx tsc --version

# 生成 tsconfig.json
npx tsc --init

ts-node 与 tsx:直接运行 TS 文件

# ts-node:传统方式,带完整类型检查
npm install --save-dev ts-node
npx ts-node src/index.ts

# tsx:更快,跳过类型检查(适合开发时热重载)
npm install --save-dev tsx
npx tsx src/index.ts
npx tsx watch src/index.ts  # 文件变化时自动重启

# 也可以直接用 Node.js 22.6+ 的原生 TS 支持(实验性)
node --experimental-strip-types src/index.ts

第一个 TypeScript 文件

// src/index.ts

// 类型标注:变量
const greeting: string = 'Hello, TypeScript!';
const year: number = 2024;
const isActive: boolean = true;

// 类型推断:TypeScript 自动推导类型,无需手写
const autoString = '这是 string 类型';      // 推断为 string
const autoNumber = 42;                       // 推断为 number
const autoArray = [1, 2, 3];               // 推断为 number[]

// 函数类型标注
function greet(name: string, age: number): string {
  return `你好,${name}!你 ${age} 岁了。`;
}

// 接口定义对象形状
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

// 使用接口
function displayUser(user: User): void {
  console.log(`[${user.id}] ${user.name} <${user.email}>`);
}

const myUser: User = {
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  createdAt: new Date(),
};

displayUser(myUser);

编译为 JavaScript

# 编译单个文件
npx tsc src/index.ts
# 生成 src/index.js(类型信息被擦除)

# 使用 tsconfig.json 编译整个项目
npx tsc

# 只做类型检查,不生成 JS 文件(推荐 CI 使用)
npx tsc --noEmit

# 监视模式:文件变化时自动重新编译
npx tsc --watch
tsc 并不是最快的构建工具

在实际项目中,tsc 的编译速度较慢(因为它会执行完整的类型检查)。大多数现代工具链(Vite、esbuild、SWC)会跳过类型检查、只做语法转译,速度快 10-100 倍。推荐的做法是:开发和构建用快速工具(Vite/esbuild),CI 流水线中专门运行 tsc --noEmit 进行完整类型检查。