首页 / TypeScript 入门教程 / 从 JavaScript 迁移

TypeScript 入门教程

从 JavaScript 迁移

本教程共 80 篇 · 第 78 篇 · 更新于 2026-08-10 · 约 14 分钟阅读

TypeScriptTypeScript 入门教程迁移allowJscheckJsd.tsJavaScript to TypeScript

本节目标:掌握把一个已有 JavaScript 项目逐步迁移到 TypeScript 的完整策略。学完你会知道从哪开始、怎么分配工作量、哪些坑需要提前绕过。

你不需要在周一早上把整个项目重写一遍。TypeScript 的迁移设计就是”渐进式”的——你可以先让 JS 和 TS 文件共存,然后一块一块地把 .js 改成 .ts。微软的 VS Code 团队当年就是这么干的。

迁移的核心思想:不是一次重写

很多人觉得”迁移到 TypeScript”就是开一个新分支、把所有文件改成 .ts、修完报错再合回去。

不要这么干。

一个 500 文件的项目,你改到第 20 个文件时类型错误可能已经几百个了。你不知道哪个是真实 bug、哪个是类型系统太严格、哪个是你的代码确实要改。这种”大爆炸式迁移”有 90% 的概率中途放弃。

正确的做法是:先让 TypeScript 认识你的 JS 代码,再逐步加类型标注,最后收紧检查选项。

第一步:不动任何代码,引入 tsconfig

先在你的 JS 项目根目录安装 TypeScript 并生成配置:

npm install -D typescript
npx tsc --init

TypeScript 7.0 默认生成的 tsconfig 里 strict 已经是 true。但你的 JS 项目大概率经不起严格模式的检查——别急,先把它关掉。打开 tsconfig.json,改几个关键选项:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": false,
    "allowJs": true,
    "checkJs": false,
    "outDir": "./dist",
    "rootDir": "./src",
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

关键选项解读:

  • allowJs: true:让 TypeScript 编译器”认识” .js 文件,允许它们和 .ts 文件共存。
  • checkJs: false:不检查 .js 文件中的类型错误。这一步先不报错,只保证能编译。
  • strict: false:先放水。类型检查后面再收紧。

整个项目的 JS 文件不需要改一行代码,tsc 就能跑起来——只是它几乎不会报任何错。这不是 bug,这是迁移策略。

Note

如果你的项目用的是 Webpack 或 Vite 打包,tsc 在这里只做”编译到 JS”这一步。实际上你的打包工具已经在做这件事了——所以初始阶段 tsc 更多是一个”占位”,为后面逐步加类型检查做准备。

第二步:渐进开启 checkJs

当你的 tsconfig 能正常跑通之后,把 checkJs 打开:

{
  "compilerOptions": {
    "checkJs": true
  }
}

tsc 会开始检查 .js 文件的类型。别被报错数量吓到——很多是 TypeScript 根据你的代码推断出来的警告,不代表你的代码有 bug。

这时候你可以做两件事:

  1. 用 JSDoc 注解给关键函数加类型,不用改文件扩展名。
  2. 挑报错最少的文件优先改成 .ts

JSDoc 加类型长这样:

// users.js —— 不用改名,加注释就行
/**
 * @param {string} name
 * @param {number} age
 * @returns {{ name: string, age: number }}
 */
function createUser(name, age) {
  return { name, age };
}

checkJs 模式下,TypeScript 会读取这些 JSDoc 注解并做类型检查。如果函数调用和注解不一致,编辑器里就会有红色波浪线。

这个阶段的优势:**降级风险为零。**所有文件还是 .js,构建工具完全不受影响,type checking 是附加的。

Tip

checkJs 是迁移的”温度计”而不是”手术刀”。先跑通、看报错分布、再决定从哪里下手。

第三步:按优先级把 .js 改成 .ts

改文件扩展名不是随便挑一个就改。有一个很实际的优先级顺序:

优先级 1:纯工具函数 / 工具模块

src/utils/formatDate.js  →  src/utils/formatDate.ts
src/utils/debounce.js    →  src/utils/debounce.ts

这些文件的特点是:**没有外部依赖(或者依赖很少)、输入输出明确、逻辑纯计算。**改起来最快、收益最高。一个 debounce 函数加上类型标注只需要 30 秒:

// 改名前
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 改名后
function debounce<T extends (...args: any[]) => void>(
  fn: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timer: ReturnType<typeof setTimeout>;
  return function (this: unknown, ...args: Parameters<T>) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

优先级 2:数据模型 / 类型定义

src/types/user.js   →  src/types/user.ts
src/models/order.js →  src/models/order.ts

这些文件定义了项目的核心数据结构。把它们改成 .ts 后,可以顺势抽出 interface 和 type——后面的文件迁移会越来越轻松,因为它们可以直接引用这些类型。

// models/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  createdAt: Date;
}

export interface CreateUserInput {
  name: string;
  email: string;
  role?: "admin" | "editor" | "viewer";
}

优先级 3:被依赖少的模块

找那些”被很多文件 import、但它自己不 import 太多东西”的模块。这种模块改完后,所有依赖它的文件都能享受到类型检查的好处——即使它们自己还是 .js

优先级 4:入口文件 / 页面组件

最后改入口。入口文件 import 了几乎所有东西,最后一个改可以确保所有依赖都已经有类型标注了。

判断依赖关系最快的方式:在 VS Code 里对某个文件右键 → “查找所有引用”(Find All References),看看有多少文件依赖它。引用越少、越纯粹的模块,越适合先改。

第四步:逐步收紧 strict

当你觉得大部分关键模块都已经有类型标注了,开始收紧检查选项——但不要一步到位,逐步开:

// 阶段一:先开这些,工作量最小、收益最高
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

noImplicitAny 要求所有变量和参数要么有显式类型标注、要么能从上下文推断出来。strictNullChecks 区分 null / undefined 和其他类型——这是 TypeScript 类型系统最有价值的能力之一。

修完这两项的报错后,再打开完整的 strict

{
  "compilerOptions": {
    "strict": true
  }
}
Note

TypeScript 7.0 的 strict 已经是默认 true 了。如果你的项目从一开始就是用 7.0 建的,那 strict 就是自动开的。这里的”逐步收紧”是针对从旧 JS 项目迁移过来的情况。

处理第三方 JS 库

你的旧 JS 项目大概率用了很多 npm 包。迁移到 TypeScript 后,import 这些库时 tsc 会报错:“找不到模块 xxx 的类型声明文件”。

解决方式有三档:

第一档:安装 @types 包(90% 的情况能搞定)

绝大多数主流 JS 库的维护者(或社区)已经提供了类型声明包,以 @types/ 为前缀发布在 npm 上:

npm install -D @types/lodash @types/react @types/express

TypeScript 会自动识别这些包。你的 import 语句不需要做任何修改。

TypeScript 7.0 的重要变更:types 的默认值从”自动引入所有 @types/*”变成了 []。如果你需要全局类型(比如 nodejest),必须在 tsconfig 里显式写出来:

{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}

第二档:库自带类型声明(开箱即用)

越来越多 JS 库直接在包里附带了 .d.ts 文件。安装后直接用就行——比如 date-fnszodimmer 等。

判断方法:看库的 package.json 里有没有 "types" 字段。有的话就是自带类型。

第三档:自己写 .d.ts(冷门库或不维护的库)

如果上述两种情况都没有——比如一个内部私有包、或者一个 2018 年后就没更新过的 npm 包——那就自己写声明文件。

在项目根目录建一个 types/ 文件夹,在里面创建同名声明文件:

// types/old-lib.d.ts
declare module "old-lib" {
  export function doSomething(input: string): number;
  export function doAnotherThing(options: {
    flag: boolean;
  }): string;
}

然后在 tsconfig 里告诉 TypeScript 这个文件夹:

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./types"]
  }
}

你不需要给整个库写完整声明——只写你实际用到的函数和接口。缺什么加什么,够用就行。

Tip

.d.ts 的时候如果类型搞错了(比如参数类型写反了),tsc 会直接报错——因为 TypeScript 会用你声明的类型去检查调用代码。这是好事,帮你发现声明和实际 API 不一致的地方。

常见踩坑

以下是 JS 项目迁移到 TypeScript 时最常遇到的几个坑。每个都能在 Stack Overflow 上搜到几十个相关问题。

坑 1:this 问题

JavaScript 里 this 的指向取决于调用方式。TypeScript 默认不知道函数里的 this 是什么类型,会报 "this" implicitly has type "any"

// ❌ 报错
class Button {
  text: string;
  handleClick() {
    console.log(this.text); // 'this' implicitly has type 'any'
  }
}

两种解法:

// 方案 A:用箭头函数(推荐)
class Button {
  text: string;
  handleClick = () => {
    console.log(this.text); // ✅ this 自动绑定
  };
}

// 方案 B:显式声明 this 参数
function handleCallback(this: HTMLButtonElement, event: MouseEvent) {
  console.log(this.textContent);
}

坑 2:动态属性

JavaScript 里随手给对象加属性是家常便饭:

const config = {};
if (isDev) {
  config.debug = true; // JS 里完全 OK
}

TypeScript 不允许对空对象随便塞属性。解法:

// 方案 A:声明时给出完整类型
interface Config {
  debug?: boolean;
  apiUrl?: string;
}
const config: Config = {};
if (isDev) {
  config.debug = true; // ✅
}

// 方案 B:用索引签名(不推荐作为首选方案)
const config: Record<string, unknown> = {};
config.debug = true; // ✅ 但失去了类型安全

坑 3:隐式 any

迁移时最常见的报错类型:TypeScript 推断不出类型,默认给了 any,但又没打开 noImplicitAny 去报错。等 strict 一开,几百条错误一起扑面而来。

早期策略:迁移阶段 noImplicitAny 可以先不开,但每改一个文件,顺手把函数参数的类型补上。与其最后一次性修 300 个 any,不如每次改文件时顺便加上类型——不痛苦、不打断节奏。

// 迁移中的 JS 写法
function processData(data, options) {
  return transform(data, options.flag);
}

// 改名成 .ts 后顺手补类型
function processData(data: DataRow[], options: ProcessOptions): Result[] {
  return transform(data, options.flag);
}

坑 4:CommonJS 的 module.exports

很多老 JS 项目使用 CommonJS 规范:

// utils.js
function add(a, b) { return a + b; }
module.exports = { add };

// 另一个文件
const { add } = require("./utils");

TypeScript 支持这种写法,但有一些注意事项:

// utils.ts —— 推荐用 ES module 语法
export function add(a: number, b: number): number {
  return a + b;
}

// 另一个文件 —— 推荐用 import
import { add } from "./utils";

如果老项目实在不想改 import 写法,可以用 export = 语法保持兼容:

// utils.ts
function add(a: number, b: number): number {
  return a + b;
}
export = { add };

// 消费方
import utils = require("./utils");
// 或
import * as utils from "./utils";

但强烈建议趁迁移的机会统一换成 ES module 的 import / export 语法——这是未来的方向,TypeScript 7.0 默认 module: "esnext" 已经说明了官方态度。

坑 5:node_modules 里的类型冲突

开了 checkJs 之后,有时候 tsc 会报 node_modules 里某个包的类型错误——不是你的代码有问题,是那个包自己的 .d.ts 有问题。

node_modules/some-lib/index.d.ts:42:5 - error TS2345: ...

解法:

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

skipLibCheck 跳过所有 .d.ts 文件(包括 node_modules 里)的类型检查。迁移初期建议打开,等你的代码全部迁移完了再考虑关掉它做更严格的检查。

迁移时间线参考

这里给一个粗略的时间线,具体取决于项目规模:

项目规模预计周期建议节奏
小(<50 文件)1–3 天连续改完,一步到位
中(50–300 文件)1–4 周每天改几个模块,不强求一气呵成
大(>300 文件)1–6 个月渐进式,按模块优先级逐个击破

关键心态:不要把迁移当成一个”做完就完了”的项目。它是一种持续改进——今天改了 5 个文件,类型覆盖率从 12% 提到 18%,这就已经是成果了。

回顾:你从这本书里学到了什么

这一章是本书”工程化与生态”的第一篇,也是全书倒数第三章。从第 1 章”TypeScript 是什么”开始,你走过了:

  1. 基础类型、数组、元组、枚举(第 8–14 章)
  2. 函数、泛型函数(第 17–22 章)
  3. 接口、类型别名、类的完整体系(第 23–37 章)
  4. 泛型进阶、工具类型、条件类型、映射类型(第 38–50 章)
  5. 类型守卫、类型兼容性等高阶话题(第 51–53 章)

然后进入工程化部分,本章教你如何把一个现有的 JavaScript 项目平滑迁移到 TypeScript。从”不动代码只加 tsconfig”到”逐步加类型标注再到全面 strict”——这条路已经被成千上万的项目验证过,你不用再摸着石头过河了。

下一步

你在读的是本书最后三章。第 79 章会讲构建工具集成——Vite、Webpack 怎么跟 TypeScript 配合。第 80 章是 TypeScript 7.0 的完整迁移指南——从 5.x/6.x 升级到 7.0 需要做什么、哪些配置需要改、框架项目该怎么处理。

读完这两章,你就不是一个只会在 Playground 里写 TypeScript 的人了——你有能力在生产环境里搭建 TypeScript 工具链、维护和升级它。