从 JS 到 TS:3类运行时错误如何在编译期拦截
第1章:从 JS 到 TS:3类运行时错误如何在编译期拦截
TypeScript 不是给变量贴标签的工具,而是在你按下保存键的瞬间拦截三类 JavaScript 运行时错误的武器。
本章核心问题:为什么 JavaScript 开发者需要 TypeScript?它到底解决了哪些真实的生产环境问题?
读完本章你将理解:
- TypeScript 能拦截的三类 JS 运行时错误(属性访问、参数类型、API 结构变更)
- TypeScript 的编译模型:类型只在编译期存在,运行时零开销
- 结构化类型系统的核心思想:形状匹配即类型兼容
Level 1 · 你需要知道的(1-3年经验)
为什么要学 TypeScript
不是因为"类型安全"这个概念听起来高级,而是因为 JavaScript 有三类错误在生产环境反复出现,TypeScript 能在你按下保存键的瞬间拦截它们。
错误类型 1:属性访问错误
// JS:运行时才崩溃
function getUsername(user) {
return user.profile.name; // TypeError: Cannot read properties of undefined
}
getUsername({ name: "Alice" }); // user.profile 是 undefined
// TS:编译期报错,0运行时代价
interface User {
name: string;
profile?: { name: string };
}
function getUsername(user: User): string {
return user.profile?.name ?? user.name; // 必须处理 undefined
}
错误类型 2:函数参数类型错误
// JS:静默失败,结果是 NaN
function add(a, b) { return a + b; }
add("5", 3); // "53" — 字符串拼接,不是加法
// TS:参数类型不匹配直接报错
function add(a: number, b: number): number { return a + b; }
add("5", 3); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
错误类型 3:API 响应结构变更
// 后端改了字段名,TS 立刻告诉你所有受影响的地方
interface ApiResponse {
userId: number; // 后端把 user_id 改成了 userId
email: string;
}
// 所有访问 response.user_id 的地方全部报红 — 而不是等用户反馈
TypeScript 的工作方式
TypeScript 是 JavaScript 的类型超集:所有合法的 JS 代码都是合法的 TS 代码。
TypeScript 源文件 (.ts)
↓ tsc 编译
JavaScript 文件 (.js) ← 浏览器/Node.js 执行的是这个
关键点:类型信息在编译后完全消失。运行时没有类型检查,没有性能开销。TypeScript 只是给你一个更聪明的编辑器和编译器。
5分钟本地环境
推荐:用 tsx 直接运行 TS 文件,不需要先编译
npm install -g tsx
# 直接运行
tsx your-file.ts
# 监听模式
tsx watch your-file.ts
正式项目:初始化 TypeScript 配置
npm init -y
npm install -D typescript @types/node
# 生成 tsconfig.json(下面专章详解每个选项)
npx tsc --init
Level 2 · 它是怎么运行的(3-5年经验)
结构化类型 vs 名义类型
这是理解 TypeScript 的核心。TypeScript 用结构(形状)判断类型兼容性,不看名字。
interface Point2D { x: number; y: number; }
interface Vector2D { x: number; y: number; }
function printPoint(p: Point2D) {
console.log(p.x, p.y);
}
const v: Vector2D = { x: 1, y: 2 };
printPoint(v); // ✅ 合法 — 结构匹配,即使类型名不同
这和 Java/C# 的名义类型完全不同——Java 里 Point2D 和 Vector2D 是不兼容的两个类型。
实际影响:
// 一个常见的"结构类型惊喜"
interface Named { name: string; }
class Dog {
name: string;
constructor(name: string) { this.name = name; }
}
function greet(n: Named) { console.log(n.name); }
greet(new Dog("Buddy")); // ✅ 合法 — Dog 有 name 属性,结构匹配
TypeScript 编译器在判断类型兼容时执行的是结构子类型检查(structural subtyping):如果 A 包含 B 所需的所有属性(可以有多余的),则 A 可以赋值给 B。这个决策来源于 JavaScript 的鸭子类型传统——"如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子。"
Level 3 · 规范怎么定义的(资深)
TypeScript 的结构化类型系统在 Anders Hejlsberg 的设计中是一个有意识的选择,目的是与 JavaScript 的运行时语义对齐。在 TypeScript 规范中,类型兼容性基于"assignability"关系,而不是"nominal identity"。这与 OCaml 的行多态(row polymorphism)、Go 的隐式接口实现有相似的设计哲学——类型兼容性由结构决定,不需要显式声明实现关系。
TypeScript 的类型擦除(type erasure)模型也值得关注:编译器在输出 JavaScript 时完全移除所有类型注解、接口、类型别名。这意味着 TypeScript 的类型系统是一个纯编译时约束系统,不在运行时创建任何额外对象。这与 Java 的泛型类型擦除类似,但 TypeScript 更彻底——连类(class)的类型信息也不保留。
Level 4 · 边界与陷阱(所有人)
any 的真实代价
any 是逃生舱,但每次用都在挖坑:
function processData(data: any) {
return data.items.map((item: any) => item.value); // 零类型检查
}
// 三个月后,data 的结构改了
// TS 不会告诉你 — 你用了 any,等于告诉编译器"我自己负责"
// 结果:生产环境 TypeError
any 有传染性:
const x: any = getExternalData();
const y = x.foo; // y 的类型也是 any
const z = y.bar.baz; // z 还是 any
// 整条链路都失去了类型保护
替代方案:用 unknown 代替 any,强制在使用前做类型检查(第02章详解)。
结构类型的陷阱:多余属性检查只在字面量赋值时生效
interface Config { host: string; port: number; }
// 字面量赋值:多余属性报错
const cfg: Config = { host: "localhost", port: 3000, debug: true };
// Error: Object literal may only specify known properties
// 变量赋值:多余属性不报错
const obj = { host: "localhost", port: 3000, debug: true };
const cfg2: Config = obj; // OK — 结构匹配,debug 被忽略
这种不一致是新手常见困惑来源。记住规则:只有对象字面量直接赋值时,TypeScript 才做多余属性检查。
本章要点
| 要点 | 说明 |
|---|---|
| TS 是 JS 超集 | 所有 JS 代码都是合法 TS 代码 |
| 类型只在编译期存在 | 运行时零开销 |
| 结构化类型系统 | 形状匹配即类型兼容,不看名字 |
| 3类错误可以拦截 | 属性访问、参数类型、API结构变更 |
避免 any |
用 unknown 代替,保持类型链路完整 |
下一章: 类型推断与标注边界 — TypeScript 能自动推断多少,哪些情况必须手动写类型标注。