第 1 章

从 JS 到 TS:3类运行时错误如何在编译期拦截

第1章:从 JS 到 TS:3类运行时错误如何在编译期拦截

TypeScript 不是给变量贴标签的工具,而是在你按下保存键的瞬间拦截三类 JavaScript 运行时错误的武器。

本章核心问题:为什么 JavaScript 开发者需要 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 里 Point2DVector2D 是不兼容的两个类型。

实际影响:

// 一个常见的"结构类型惊喜"
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 能自动推断多少,哪些情况必须手动写类型标注。

本章评分
4.7  / 5  (108 评分)

💬 留言讨论