跳到主要内容
预计阅读 21 分钟

项目启动前的技术选型:为什么团队需要TypeScript

需求场景

你正在筹备一个中型 Web 应用的开发。产品经理列出了数十个功能模块,后端同事已经开始输出 API 文档,前端团队有五名开发者将要协同编码。项目周期六个月,上线后还需要长期迭代。

技术负责人把你拉进会议室,问了一个直接的问题:这个项目用 JavaScript 还是 TypeScript?

这不是一个简单的个人偏好问题。它关系到整个项目的可维护性、协作效率和上线质量。要做出正确的判断,需要先弄清楚 TypeScript 到底是什么,以及它在工程实践中解决了哪些真实痛点。

TypeScript 的工程定位

TypeScript 是 JavaScript 语言的一个扩展层。它在 JavaScript 的完整语法之上叠加了一套静态类型系统,使得代码在编写阶段就能接受类型层面的校验。

关键特性一览:

  • 任何合法的 JavaScript 代码直接放进 .ts 文件都可以编译通过,二者是包含关系
  • 所有类型标注在编译为 JavaScript 后被彻底移除,运行时不存在任何额外负担
  • 编译器 tsc 负责两件事:检查类型正确性,以及把 TypeScript 语法转为目标版本的 JavaScript

用一个图来表示编译流程:

  源码 (.ts)
      |
  tsc 编译器
   /        \
类型检查    代码转换
   |           |
错误报告    输出 (.js)

这意味着 TypeScript 不是一门独立的新语言,而是 JavaScript 开发流程中的一个质量保障工具。

五人协作时 JavaScript 暴露的问题

回到开会场景。如果你选择纯 JavaScript 开发,以下问题几乎必然出现。

问题一:接口对接时的参数歧义

后端给出一个用户查询接口,返回的字段中 score 有时是数字、有时是字符串。前端开发者 A 按数字处理,开发者 B 按字符串处理。两个人的代码在各自的测试用例下都能跑通,但在联调时产生数据不一致的故障。

// 开发者 A 的写法
function renderScore(val) {
  return val.toFixed(1);
}

// 开发者 B 的写法
function renderScore(val) {
  return val.substring(0, 4);
}

运行到不匹配的那一路时,两种写法都会崩溃。

问题二:重构时的连锁反应

项目进入第三个月,产品提出需要修改订单模块的数据结构。开发者 C 改完了核心模块,但遗漏了三个引用该结构的辅助函数。这些遗漏直到 QA 测试阶段才被发现,修复成本是原来的三倍。

问题三:代码审查效率低下

开发者 D 提交了一个 PR,其中一个工具函数接收的参数既可能是数组又可能是单个对象,但函数内部直接调用了 .map() 方法。代码审查人没有注意到这个隐患,代码合入主分支后引发了线上告警。

这些问题的根源是同一个:JavaScript 在编码阶段不做类型层面的约束。团队越大、代码量越多、迭代周期越长,这种约束缺失的代价就越高。

TypeScript 如何应对

TypeScript 的静态类型系统针对上述场景提供了结构化的解决方案。

应对一:编译期捕获类型冲突

// 明确约定 score 的类型
function renderScore(val: number): string {
  return val.toFixed(1);
}

renderScore("95"); // 编译报错:参数类型 string 不能赋给 number

类型声明充当了团队内部的契约。即使后端返回的数据类型不确定,也可以在类型定义中明确表达:

interface QueryResult {
  score: number | string;
}

这样所有使用 score 的地方都会被编译器要求先做类型判断再处理,遗漏的可能性大幅降低。

应对二:重构时的全局影响分析

修改数据结构时,TypeScript 编译器会在所有引用该结构的位置标注错误。以一个订单对象为例:

interface OrderItem {
  productCode: string;
  quantity: number;
  unitPrice: number;
}

// 当 productCode 被改名为 sku 时,
// 所有引用 productCode 的代码都会立即报错

开发者不需要全局搜索、逐个排查,编译器帮你完成了这项工作。

应对三:编辑器提供精确的代码导航

类型信息让 IDE 能够精确地展示一个变量拥有哪些属性、一个函数需要什么参数、返回什么结果。开发者 D 在写代码时就会收到提醒:传入的参数可能不是数组,无法直接调用 .map()

应对四:类型声明就是接口文档

下面这个函数签名不需要任何注释就能传达完整信息:

function calculateShipping(
  weight: number,
  destination: string,
  express: boolean
): number {
  // ...
}

参数类型、返回值类型一目了然。新加入团队的成员查看类型声明就能理解 API 的用法。

真实的成本核算

引入 TypeScript 不是零代价的。在技术选型会议上你需要向团队坦诚这些成本。

学习曲线: 团队成员需要花时间理解类型标注语法、泛型、接口等概念。对于纯 JavaScript 背景的开发者,大约需要一到两周适应基本用法。

开发节奏变化: 编写类型声明需要额外的代码量。特别是与第三方库交互时,有时需要安装额外的类型声明包或自行编写 .d.ts 文件。

编译环节: 代码不能直接在浏览器或 Node.js 中运行,必须先经过 tsc 编译。不过现代工具链(如 Vite、esbuild)已经将这一步骤的耗时压缩到毫秒级别。

灵活性受限: 某些动态特性较强的编码手法在严格模式下会被编译器拒绝,需要改用更规范的写法。

但对于五人以上协作、六个月以上周期的项目来说,这些前期投入换来的收益远超成本。

搭建开发环境

技术选型确定后,下一步是在项目中配置 TypeScript 工具链。

安装编译器

TypeScript 编译器基于 Node.js 运行。确认 Node.js 已安装后,在终端执行:

node -v    # 确认 Node.js 已安装
npm -v     # 确认 npm 可用

在项目中安装 TypeScript 作为开发依赖:

mkdir order-system && cd order-system
npm init -y
npm install --save-dev typescript

将 TypeScript 安装为项目级依赖而非全局安装,可以确保团队所有成员使用同一版本的编译器,避免因版本差异导致编译行为不一致。

验证安装:

npx tsc -v

快速执行工具

开发阶段频繁执行「编译 -> 运行」的两步操作效率偏低。社区提供了多种”一步运行”工具。

推荐使用 tsx(基于 esbuild,启动极快,兼容性好):

npm install --save-dev tsx
npx tsx src/main.ts   # 直接运行 .ts 文件

另一个经典方案是 ts-node,功能更丰富但启动稍慢,且对某些 TypeScript 特性(如 ESM)的支持需要额外配置:

npm install --save-dev ts-node
npx ts-node src/main.ts

ts-node 还提供交互模式(REPL),适合快速验证类型行为:

npx ts-node
> const total: number = 100
> console.log(total * 0.8)
80

Ctrl + D 退出。

此外,Node.js 22.6+ 已内置对 TypeScript 的实验性支持,可通过 --experimental-strip-types 标志直接运行 .ts 文件,无需额外工具。Node.js 23.6+ 和 22.18+ 已默认启用该功能,无需任何标志即可直接运行 .ts 文件。

编写第一个模块

创建项目入口文件 src/main.ts

function formatCurrency(amount: number, currency: string): string {
  return `${currency} ${amount.toFixed(2)}`;
}

const orderTotal: number = 299.5;
console.log(formatCurrency(orderTotal, "CNY"));

手动编译并运行:

npx tsc src/main.ts
node src/main.js
# 输出:CNY 299.50

或者用 tsx 一步完成:

npx tsx src/main.ts

体验类型检查

故意传入错误类型的参数:

function formatCurrency(amount: number, currency: string): string {
  return `${currency} ${amount.toFixed(2)}`;
}

formatCurrency("free", 100);
// error TS2345: Argument of type 'string' is not assignable
// to parameter of type 'number'.

错误在编译阶段就被拦截,不需要等到运行时。

需要注意:默认情况下即使编译报错,tsc 仍然会输出 JavaScript 文件。这是为了兼容渐进式迁移场景。可以通过配置 noEmitOnError 来改变这个行为。

运行时校验仍然必要

TypeScript 的类型检查仅限于编译阶段。对于来自外部的数据(用户输入、HTTP 响应、第三方 SDK 的回调),类型标注无法覆盖运行时的真实情况,仍然需要在代码中做防御性检查:

function formatCurrency(amount: number, currency: string): string {
  if (typeof amount !== "number" || isNaN(amount)) {
    throw new Error("amount must be a valid number");
  }
  return `${currency} ${amount.toFixed(2)}`;
}

项目配置文件

当项目包含多个源文件时,逐个指定编译参数不现实。tsconfig.json 是 TypeScript 的项目级配置文件,集中管理所有编译选项。

生成配置文件

npx tsc --init

这会在项目根目录生成一个带有详细注释的 tsconfig.json。以下是一份适用于中型项目的精简配置:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "node16",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./build",
    "rootDir": "./src",
    "noEmitOnError": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

配置项速查

配置项作用建议设定
target输出的 JavaScript 版本es2020 或更高
module模块系统Node.js 项目用 node16nodenext,纯浏览器项目用 esnext
strict启用全部严格检查true
outDir编译产物目录./build./dist
rootDir源码根目录./src
noEmitOnError有错误时阻止输出true
sourceMap生成调试映射文件开发时 true
esModuleInterop兼容 CommonJS 导入语法true

使用配置文件编译

配置就绪后,在项目根目录直接执行:

npx tsc

编译器自动读取 tsconfig.json,处理 include 指定的所有文件,并将产物输出到 outDir

监听模式

开发过程中使用 --watch 参数,编译器会在文件保存时自动重新编译:

npx tsc --watch

推荐的目录布局

order-system/
  src/
    main.ts
    utils/
      format.ts
      validate.ts
  build/           # 编译产物,gitignore
  tsconfig.json
  package.json
  node_modules/

编辑器配置

Visual Studio Code 内置了 TypeScript 语言服务,打开 .ts 文件即可获得以下能力:

  • 错误标注:类型不匹配的地方用红色波浪线标记
  • 自动补全:输入对象名后按 . 展示所有可用属性
  • 悬停提示:鼠标停留在变量上方可查看推断出的类型
  • 定义跳转:按住 Cmd/Ctrl 点击可跳转到类型定义
  • 重构辅助:右键菜单提供变量重命名、函数提取等操作

推荐在项目中添加 .vscode/settings.json

{
  "editor.formatOnSave": true,
  "typescript.tsdk": "node_modules/typescript/lib",
  "editor.codeActionsOnSave": {
    "source.organizeImports": "explicit"
  }
}

其中 typescript.tsdk 配置让编辑器使用项目本地安装的 TypeScript 版本,与团队其他成员保持一致。

调试配置

tsconfig.json 中开启 sourceMap 后,可以在 VS Code 中直接对 .ts 文件设置断点。创建 .vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug TS",
      "program": "${workspaceFolder}/src/main.ts",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "outFiles": ["${workspaceFolder}/build/**/*.js"],
      "sourceMaps": true
    }
  ]
}

按 F5 启动调试,断点、单步执行、变量查看均可在 TypeScript 源码层面进行。Source Map 文件(.js.map)负责建立编译产物和源码之间的映射关系。

常见疑问

TypeScript 和 JavaScript 能在同一个项目中共存吗?

可以。在 tsconfig.json 中设置 "allowJs": true.ts.js 文件就可以混合存在。这是渐进式迁移的基础策略:每次只把一两个文件从 .js 改为 .ts,逐步推进。

第三方库没有类型声明怎么办?

社区维护了一个大型类型声明仓库。大多数流行库都有对应的类型包,安装方式为:

npm install --save-dev @types/express
npm install --save-dev @types/lodash

安装后编译器自动识别该库的 API 类型。

TypeScript 会影响运行时性能吗?

不会。编译产物就是标准的 JavaScript,所有类型标注在编译时被完全移除。运行时的代码和手写的 JavaScript 没有任何区别。

编译时的 let 为什么变成了 var

这取决于 target 配置。如果目标版本低于 ES2015,编译器会将 letconst 降级为 var。设置 targetes2015 或更高版本即可保留原始语法。

本章回顾

要点说明
工程定位TypeScript 是 JavaScript 的超集,在编码阶段提供类型校验
核心价值编译期发现类型错误,降低多人协作的沟通和维护成本
编译流程.ts 文件经 tsc 编译后输出 .js 文件,类型声明被完全移除
项目级安装npm install --save-dev typescript,确保团队版本一致
快速执行tsx(推荐)或 ts-node 跳过手动编译步骤直接运行 TypeScript
项目配置tsconfig.json 集中管理编译选项,推荐开启 strict
编辑器集成VS Code 内置 TypeScript 支持,开箱可用
渐进式迁移设置 allowJs: true.ts.js 可以共存
运行时行为与纯 JavaScript 完全一致,类型系统不影响运行时性能

购买课程解锁全部内容

告别类型错误:12 章掌握 TypeScript 工程实战

¥29.90