为什么需要 TypeScript

JavaScript 的"安全帽",让错误在编写时就被发现

🎯 学习目标

学完本章,你将能够:

  • 能够理解 JavaScript 动态类型带来的常见痛点与隐患
  • 能够阐述 TypeScript 静态类型检查的核心价值与三大优势
  • 能够理解 TypeScript 的编译原理与类型擦除机制
  • 能够制定渐进式迁移策略将 JS 项目迁移到 TS
  • 能够权衡 TypeScript 的取舍,在合适场景做出技术选型

你有没有遇到过

代码跑起来才报错,而且报错位置离真正的问题十万八千里?

比如调用 user.nmae(拼错了),JS 不会在写代码时提醒你,直到用户看到页面上显示 undefined。

或者 "1" + "2" 得到 "12" 而不是 3,这种隐式转换的 bug 找起来特别费劲。

TypeScript 就是为了解决这些问题而生的——它在编写代码时就能发现错误,而不是等到运行时。

Part 1: JS 工程痛点

看看 JavaScript 在大型项目中会遇到哪些让人头疼的问题

写个 user.nmae(拼错了),JS 不会提醒你,直到用户看到页面上显示 undefined。"1" + "2" 得到 "12" 而不是 3,这种 bug 找起来特别费劲。这一节我们先用 JS 写几个"会出事"的真实案例,看看没有类型系统保护时代码有多脆弱。

痛点一:拼写错误

运行时才发现,为时已晚

这段 JS 代码不会报错,但结果永远是 undefined:

// JS:拼写错误,不会报错
function greet(user) {
    return "Hello, " + user.nmae; // nmae 拼错了!
}
greet({ name: "张三" }); // "Hello, undefined"
问题:JS 在运行时才访问 nmae 属性,发现对象上没有这个属性就返回 undefined。如果这段代码在支付流程里呢?

痛点二:类型陷阱

"1" + "2" 等于 "12",不是 3

// JS:参数类型不确定,结果不可预测
function add(a, b) {
    return a + b;
}

add(1, 2);       // 3 —— 正常
add("1", "2");   // "12" —— 字符串拼接
add(1, "2");     // "12" —— 混合更乱
add([], {});     // "[object Object]" —— 完全失控

JS 不会告诉你参数应该传什么类型。你传字符串、数字、甚至数组,它都不吭声,只是默默给你一个意料之外的结果。

痛点三:对象不确定

接口返回的数据,你确定有 email 字段吗?

// JS:API 返回什么字段,完全靠猜
fetch("/api/user")
    .then(res => res.json())
    .then(user => {
        console.log(user.email); // 如果 API 返回 mail 而非 email?
        // 不会报错,只是默默 undefined
    });

后端改了字段名,前端不知道,代码静默失败。这种 bug 在多人协作的项目中特别常见。

痛点总结

五大问题,每一个都在等你上线后才发现

拼写错误

属性名拼错,运行时才发现。

类型转换

隐式转换导致逻辑错误。

重构困难

改一个字段名,不知道影响哪些文件。

缺乏提示

IDE 不知道对象有哪些属性。

接口模糊

团队协作时,数据结构全靠口头约定。

JSDoc 的困境

既然 JS 有痛点,能不能用注释加上类型?

很多团队的第一反应是:用 JSDoc 注释来标注类型。这看起来是个轻量方案,不用改工具链。但实际用起来呢?

/**
 * 计算订单总价
 * @param {Array<{price: number, quantity: number}>} items - 商品列表
 * @param {{code: string, discount: number}} coupon - 优惠券
 * @returns {number} 总价
 */
function calcTotal(items, coupon) {
    const sub = items.reduce((s, i) => s + i.price * i.qty, 0);
    // qty 写错了,JSDoc 不会报错!
    const off = coupon.dicsount * 0.1;
    // dicsount 拼错了,JSDoc 也不会报错!
    return sub - off;
}

同样的功能,用 TypeScript 写:

interface CartItem { price: number; quantity: number; }
interface Coupon { code: string; discount: number; }

function calcTotal(items: CartItem[], coupon: Coupon): number {
    const sub = items.reduce((s, i) => s + i.price * i.quantity, 0);
    // 拼写错误?编辑器立即标红
    const off = coupon.discount * 0.1;
    return sub - off;
}
对比维度JSDocTypeScript
编译时校验可选(需 @ts-check),校验有限强制校验,覆盖全面
复杂类型写法冗长,嵌套难读interface、泛型,清晰直观
重构支持无自动重构重命名、提取接口一步到位
泛型能力几乎不支持完整泛型系统
注释与代码同步注释可能过时,无人维护类型即文档,始终与代码同步
核心问题:JSDoc 是注释,不是代码。注释可以错、可以过时、可以忘记更新。TypeScript 的类型是编译时强制检查的,错了就过不了编译。

Part 2: TypeScript 来了

TypeScript 是 JavaScript 的超集,加了静态类型检查。写代码时就能发现错误

JS 的痛点看够了,该看解决方案了。TypeScript 不是一门新语言,它只是在 JS 上面加了一层类型标注。编译的时候类型信息被擦掉,浏览器和 Node.js 看到的还是纯 JS。这一节你会看到同样的代码用 TS 写是什么样,体验一下"还没运行就知道哪里有错"的快感。

编译哲学

类型注解写完就被擦除,输出纯 JavaScript

TypeScript 是编译时工具,不改变运行时行为。编译 = 类型检查 + 类型擦除,输出纯 JS。

// TS 代码
function greet(name: string): string {
    return `Hello, ${name}`;
}
// 编译后的 JS(类型注解被完全擦除)
function greet(name) {
    return `Hello, ${name}`;
}

用人话说:TypeScript 就像一位严格的编辑,在你写稿时标注错误。交稿时,所有标注都被擦掉,只留下干净的正文。读者(浏览器/Node.js)看到的还是纯 JavaScript。

但和真正的编辑不同:编辑可能漏掉错误、标注也可能没标全;而 TypeScript 的类型检查是机械的——只要类型对得上就不报错,类型对不代表逻辑对。比如 calculateArea("circle", -5) 传了一个合法的字符串和数字,TS 不会拦你,但负数半径在业务上是错的。类型擦除只是"删掉标注",运行时一点类型保护都没有——所以第 17 章的错误处理依然必不可少。

对比:计算面积

同样的功能,JS 和 TS 的写法对比

JS 写法——运行时才能发现错误:

// JS 版本:拼写错误、遗漏分支都不会提示
function calculateArea(shape) {
    if (shape.type === "circle") {
        return Math.PI * shape.radius * shape.radius;
    }
    return shape.width * shape.height;
    // 如果传入 { type: "unknown" }?返回 NaN
}

TS 写法——编写时就能发现所有问题:

// TS 版本:编译时就能发现错误
interface Circle { type: "circle"; radius: number; }
interface Rectangle { type: "rectangle"; width: number; height: number; }
type Shape = Circle | Rectangle;

function calculateArea(shape: Shape): number {
    switch (shape.type) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
    }
}
// 拼写错误立即标红、自动补全属性、遗漏分支报警告

TS 的三大优势

为什么越来越多项目选择 TypeScript

编译时检查

拼写错误、类型错误在编写时就被发现,不用等到运行时。

智能补全

IDE 知道对象有哪些属性和方法,输入 . 就自动列出。

安全重构

改一个字段名,所有引用处都标红,不会漏改。

静态 vs 动态

一张表看清 JS 和 TS 的核心差异

特性动态类型(JS)静态类型(TS)
类型检查时机运行时编译时
类型标注不需要需要
重构安全低高
IDE 支持有限智能补全、重构
灵活性高中等
学习成本低中等
大型项目维护困难可维护性强

工业界采用

谁在用 TypeScript?几乎所有主流框架和公司

2024 年,TypeScript 在 npm 的周下载量已经超过 5000 万,是 JavaScript 生态中最受欢迎的静态类型方案。

前端框架

Angular 默认 TS;Vue 3 源码用 TS 重写;React 社区普遍使用 TS。

后端与全栈

NestJS、tRPC、Prisma 等 Node.js 框架全面拥抱 TS。

大厂实践

Microsoft、Google、Airbnb、Slack 等公司核心项目均采用 TS。

开源趋势

GitHub 上 TS 已成为第 4 大语言,npm 上超过 30% 的包提供类型定义。

图:TypeScript 生态全景

迁移案例

这些知名项目都从 JavaScript 迁移到了 TypeScript

TypeScript 不是纸上谈兵——看看这些你天天在用的项目是怎么迁移的:

VS Code —— Microsoft

VS Code 从第一天就用 TS 编写。150 万行 TS 代码,证明了 TS 在大型桌面应用中的可行性。微软内部统计:TS 帮他们减少了 15% 的 bug 修复时间。

Jest —— Facebook(Meta)

2019 年 Jest 团队启动 TS 迁移,采用渐进式策略:先用 JSDoc + @ts-check,再逐文件改为 .ts。整个迁移耗时约 6 个月,没有中断任何版本发布。

Deno —— Ryan Dahl

Node.js 作者 Ryan Dahl 在创造 Deno 时,直接把 TS 作为一等公民语言。Deno 原生支持运行 .ts 文件,无需手动编译配置。

Next.js —— Vercel

Next.js 从 v9 开始支持 TS,v10 起内部核心代码全面使用 TS。创建新项目时 npx create-next-app@latest 默认启用 TypeScript。

npm 下载趋势:2018 年 TypeScript 的 npm 周下载量约 400 万;2022 年突破 3000 万;2025 年已超过 5000 万/周。7 年增长超过 12 倍,增速远超同期任何其他 JS 工具。

渐进式迁移

JS 项目不用重写,一步步"渗透"就行

你不需要一天就把整个项目改成 TS。推荐分四步走:

第一步:改后缀

把 .js 改成 .ts,不需要加任何类型注解。TS 兼容所有合法 JS。

第二步:开 strict

在 tsconfig.json 中开启 strict: true,让编译器帮你找出问题。

第三步:加注解

从公共接口(API 响应、函数签名)开始,逐步添加类型注解。

第四步:消 any

把临时用的 any 逐步替换为具体类型或 unknown。

图:JS 到 TS 渐进迁移流程

迁移阶段一

允许 JS 和 TS 混合共存,零风险起步

第一阶段的目标很简单:让项目能编译通过,不需要改任何类型。关键是配置 tsconfig.json 允许 JS 和 TS 文件共存。

// tsconfig.json —— 阶段一:混合模式
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,
    "noImplicitAny": false,
    "strict": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

allowJs: true

允许编译器处理 .js 文件。你的 JS 代码不用改任何东西就能和 TS 文件一起参与编译。

noImplicitAny: false

允许隐式 any 类型。没有类型注解的参数默认是 any,不会报错。这样你才能逐步加类型。

这一阶段你可以做:新文件用 .ts,旧文件保持 .js 不动。项目照样编译、照样运行。先让脚迈进门槛,再慢慢来。

迁移二三阶段

从宽松检查到严格模式,逐步收紧

阶段二:开启基本检查,开始给公共接口加类型注解。

// tsconfig.json —— 阶段二:基本检查
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strict": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

阶段三:全面开启严格模式。这是大多数团队的目标状态。

// tsconfig.json —— 阶段三:严格模式
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": false,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}
常见陷阱:不要直接从阶段一跳到阶段三!strict: true 会一次性开启所有严格检查,可能产生几百个报错。按阶段走,每阶段修完所有错误再进入下一阶段。

迁移阶段四

极致严格:让编译器帮你挡住更多边界情况

当团队对 TS 已经很熟悉后,可以开启更严格的选项,让类型系统发挥最大威力:

// tsconfig.json —— 阶段四:极致严格
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitOverride": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

noUncheckedIndexedAccess

数组索引和对象键访问返回 T | undefined,防止越界访问。arr[0] 不再假定一定存在。

exactOptionalPropertyTypes

可选属性(age?: number)只能省略或赋具体值,不能显式赋 undefined。让"故意不存在"和"明确为空"区分开,语义更精确。若确需赋 undefined,类型要写成 age?: number | undefined。

noImplicitOverride

子类重写父类方法必须加 override 关键字,防止意外覆盖。

阶段四不是必需的。大多数项目停在阶段三就够了。只有金融、医疗等对安全性要求极高的项目才建议上到阶段四。

ts-check 方案

不改后缀、不配编译器,也能享受类型检查

如果你的项目非常大(几十万行 JS),连阶段一都不想碰,还有一个更轻量的选择:// @ts-check。

// @ts-check
// 在 JS 文件顶部加这一行,VS Code 就会启动类型检查

/** @type {number} */
let count = 0;

/**
 * 计算折扣价
 * @param {number} price - 原价
 * @param {number} discount - 折扣率(0-1)
 * @returns {number}
 */
function getSalePrice(price, discount) {
    return price * (1 - discount);
}

// 如果这样调用,编辑器会标红:
getSalePrice("99", 0.2); // 错误:"99" 不是 number
count = "hello";          // 错误:不能把 string 赋给 number

优点

零配置、零迁移成本。在现有 .js 文件中加一行注释就能用。适合超大型遗留项目。

局限

类型写在注释里,不如 TS 直观;不支持复杂泛型;IDE 提示有限。这只是过渡方案,最终还是要迁移到 .ts。

注意:@ts-check 只在编辑器里检查,不会阻止代码运行。它是一个"提醒",不是"门槛"。适合做迁移的跳板,不适合做终点。

版本选择

TypeScript 版本那么多,该用哪个?

TypeScript 采用定期发布模式,大约每 2 个月发布一个次要版本(如 5.4、5.5、5.6)。没有传统意义上的 LTS 版本,但每个发布版都经过充分测试,可以放心使用。

# 查看当前安装的版本
$ npx tsc --version
# TypeScript 5.7

# 安装指定版本
$ npm install typescript@5.6 --save-dev

# 安装最新版
$ npm install typescript@latest --save-dev

新项目:用最新版

直接用 npm install typescript@latest。最新版有最好的类型推断、最快的编译速度和最新的语言特性。

老项目:保守升级

跟随项目现有版本,小版本升级(5.4 → 5.5)。每个版本的 Breaking Changes 都很少,官方会详细列出。

库作者:用 typesVersions

如果你的包发布到 npm,在 package.json 中用 typesVersions 为不同 TS 版本提供对应的类型定义。

// package.json —— 库的 typesVersions 配置
{
  "name": "my-lib",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "typesVersions": {
    ">=5.0": { "*": ["dist/ts5.0/*"] },
    "*":     { "*": ["dist/legacy/*"] }
  }
}

编译流程

.ts 文件是怎么变成 .js 的?三步走

TypeScript 编译器(tsc)的工作可以拆成三步:

  .ts 源码
     ↓
  ① 解析 → 生成 AST(抽象语法树)
     ↓
  ② 类型检查 → 检查所有类型是否匹配
     ↓
  ③ 类型擦除 → 移除注解,输出纯 .js
// 输入:src/hello.ts
const name: string = "张三";
function greet(n: string): string {
    return `Hello, ${n}`;
}
// 输出:dist/hello.js
const name = "张三";
function greet(n) {
    return `Hello, ${n}`;
}
图:TypeScript 编译管线

综合练习

找出下面 JS 代码中的所有类型隐患

// 一个"订单处理"函数,藏着多少类型隐患?
function processOrder(order) {
    const total = order.items.reduce((sum, item) => {
        return sum + item.price * item.quantity;
    }, 0);

    const discount = order.coupon * 0.1;
    const finalPrice = total - discount;

    order.customer.balance -= finalPrice;
    sendEmail(order.customer.emial, "订单确认");

    return { sucess: true, price: finalPrice };
}

processOrder({
    items: [{ price: "99", quantity: 2 }],
    coupon: null,
    customer: { balance: 500, email: "test@test.com" }
});

参考答案

// 类型隐患清单:
// 1. item.price 是字符串 "99",乘法结果变成 NaN
// 2. order.coupon 是 null,null * 0.1 得到 0(碰巧没报错)
// 3. order.customer.emial 拼写错误(应该是 email)
// 4. balance 可能为负数,没有校验
// 5. 返回值 sucess 拼写错误(应该是 success)
// 6. reduce 初始值 0 没问题,但 item.price 如果是 undefined 就炸了
// 这些问题 TS 都能在编译时发现!

常见疑问

TS 会拖慢开发速度吗? 短期会——你需要写类型注解。但长期来看,它帮你省下的调试时间远超写类型的成本。

疑问一:TS 是不是要重写所有代码?

不是。所有合法的 JS 都是合法的 TS。你可以把 .js 改成 .ts,就能开始用。类型注解可以逐步添加。

疑问二:TS 在浏览器里能直接运行吗?

不能。TS 需要编译成 JS 才能运行。但这个过程通常由构建工具(如 Vite)自动完成。

疑问三:学习 TS 之前需要精通 JS 吗?

不需要。边学 JS 边学 TS 反而更好,TS 能帮你更早发现 JS 代码里的错误。

试一试

练习一

任务:找出下面 JS 代码中的所有"类型相关"隐患。

function getUserInfo(id) {
    const user = database.find(id);
    return user.age + "岁";
}
getUserInfo("abc");

参考答案

// 隐患:
// 1. id 可能是字符串,但数据库可能期望数字
// 2. user 可能是 null(找不到),访问 .age 会报错
// 3. user.age 如果是 undefined,结果变成 "undefined岁"

练习二

任务:把下面 JS 函数改写成 TS,添加类型注解。

// JS 版本
function formatPrice(price) {
    return "¥" + price.toFixed(2);
}

参考答案

// TS 版本
function formatPrice(price: number): string {
    return "¥" + price.toFixed(2);
}
// 现在传入字符串会编译报错

📝 要点总结

本章核心知识点

JS 痛点

拼写错误、类型陷阱、对象属性不确定——运行时才暴露

TS 优势

编译时错误提示、IDE 智能提示、重构安全保障

类型擦除

TS 编译为 JS 后类型消失,运行时无类型信息

渐进迁移

JSDoc → allowJs → 重命名 .ts → strict 模式

生态采用

主流框架与库均提供 TS 类型支持

下一章

现在你知道了为什么需要 TypeScript。下一章我们来搭建开发环境,写出第一个 .ts 文件。

1 / X