Skip to content

从 JavaScript 迁移到 TypeScript

概述

将现有的 JavaScript 项目迁移到 TypeScript 是一个渐进式的过程,不需要一次性重写所有代码。TypeScript 的设计允许你逐步添加类型,同时保持代码的可运行性。本文档提供了从 JavaScript 迁移到 TypeScript 的完整指南,包括迁移策略、具体步骤、常见问题和最佳实践。

迁移策略

策略 1:渐进式迁移(推荐)

逐步迁移是最安全和最实用的方法,特别适合大型项目:

  1. 保持现有代码运行:TypeScript 允许 .js.ts 文件共存
  2. 逐步添加类型:从新文件开始,逐步为旧文件添加类型
  3. 逐步启用严格模式:先启用基本检查,再逐步启用更严格的检查

策略 2:新项目直接使用 TypeScript

对于新项目,直接使用 TypeScript 是最佳选择:

  • 从一开始就享受类型安全
  • 避免后续迁移成本
  • 团队可以统一使用 TypeScript

策略 3:混合模式

在大型项目中,可以保持部分 JavaScript 代码,只对关键模块使用 TypeScript:

  • 核心业务逻辑使用 TypeScript
  • 工具脚本可以保持 JavaScript
  • 通过配置文件控制编译范围

迁移步骤

步骤 1:安装和配置 TypeScript

首先安装 TypeScript 并创建配置文件:

bash
# 安装 TypeScript
npm install --save-dev typescript

# 安装类型定义(如果需要)
npm install --save-dev @types/node

创建 tsconfig.json 配置文件:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "allowJs": true,
    "checkJs": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

关键配置说明

  • allowJs: true:允许编译 JavaScript 文件
  • checkJs: false:不检查 JavaScript 文件的类型错误
  • strict: false:初始阶段不启用严格模式

步骤 2:重命名文件(可选)

你可以选择性地将 .js 文件重命名为 .ts

bash
# 重命名单个文件
mv src/utils.js src/utils.ts

# 批量重命名(谨慎使用)
find src -name "*.js" -exec sh -c 'mv "$1" "${1%.js}.ts"' _ {} \;

注意

重命名文件后,TypeScript 会开始检查这些文件。如果有很多错误,可以先不重命名,保持 .js 文件,只对新文件使用 .ts

步骤 3:添加类型注解

逐步为代码添加类型注解,从最简单的开始:

示例 1:基础类型注解

typescript
// 之前:JavaScript
function greet(name) {
  return `Hello, ${name}`;
}

// 之后:TypeScript
function greet(name: string): string {
  return `Hello, ${name}`;
}

示例 2:对象类型注解

typescript
// 之前:JavaScript
function createUser(name, age) {
  return {
    name: name,
    age: age,
    isActive: true
  };
}

// 之后:TypeScript
interface User {
  name: string;
  age: number;
  isActive: boolean;
}

function createUser(name: string, age: number): User {
  return {
    name,
    age,
    isActive: true
  };
}

示例 3:函数类型注解

typescript
// 之前:JavaScript
function processData(data, callback) {
  const result = data.map(item => item * 2);
  callback(result);
}

// 之后:TypeScript
function processData(
  data: number[],
  callback: (result: number[]) => void
): void {
  const result = data.map(item => item * 2);
  callback(result);
}

步骤 4:处理第三方库

为第三方库安装类型定义:

bash
# 安装常用库的类型定义
npm install --save-dev @types/lodash @types/express @types/react

# 如果库没有类型定义,创建声明文件
# types/custom-library.d.ts
declare module 'custom-library' {
  export function doSomething(): void;
}

步骤 5:逐步启用严格模式

在代码基本迁移完成后,逐步启用严格检查:

json
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,        // 第一步:禁止隐式 any
    "strictNullChecks": false,    // 暂时不启用
    "strictFunctionTypes": false, // 暂时不启用
    "strictPropertyInitialization": false
  }
}

然后逐步启用更多检查:

json
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "strictNullChecks": true,     // 第二步:启用空值检查
    "strictFunctionTypes": true,   // 第三步:启用函数类型检查
    "strictPropertyInitialization": true
  }
}

最后启用完整严格模式:

json
{
  "compilerOptions": {
    "strict": true  // 启用所有严格检查
  }
}

常见迁移场景

场景 1:处理动态属性

JavaScript 中经常使用动态属性,需要适当处理:

typescript
// 之前:JavaScript
const config = {};
config.apiUrl = 'https://api.example.com';
config.timeout = 5000;

// 方案 1:使用接口定义
interface Config {
  apiUrl: string;
  timeout: number;
}

const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

// 方案 2:使用索引签名(如果需要动态属性)
interface Config {
  [key: string]: string | number;
  apiUrl: string;
  timeout: number;
}

const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};
config.retries = 3; // 允许动态添加属性

场景 2:处理可选参数和默认值

typescript
// 之前:JavaScript
function createUser(name, age, email) {
  return {
    name: name || 'Unknown',
    age: age || 0,
    email: email || ''
  };
}

// 之后:TypeScript
interface User {
  name: string;
  age: number;
  email?: string; // 可选属性
}

function createUser(
  name: string = 'Unknown',
  age: number = 0,
  email?: string
): User {
  return {
    name,
    age,
    ...(email && { email }) // 条件添加属性
  };
}

场景 3:处理数组和对象操作

typescript
// 之前:JavaScript
function processItems(items) {
  return items
    .filter(item => item.active)
    .map(item => item.value * 2)
    .reduce((sum, val) => sum + val, 0);
}

// 之后:TypeScript
interface Item {
  active: boolean;
  value: number;
}

function processItems(items: Item[]): number {
  return items
    .filter((item): item is Item => item.active)
    .map(item => item.value * 2)
    .reduce((sum, val) => sum + val, 0);
}

场景 4:处理异步代码

typescript
// 之前:JavaScript
async function fetchData(url) {
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

// 之后:TypeScript
interface ApiResponse {
  status: number;
  data: unknown;
}

async function fetchData(url: string): Promise<ApiResponse> {
  const response = await fetch(url);
  const data = await response.json();
  return {
    status: response.status,
    data
  };
}

场景 5:处理类和继承

typescript
// 之前:JavaScript
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  speak() {
    return `${this.name} barks`;
  }
}

// 之后:TypeScript
class Animal {
  constructor(public name: string) {}
  
  speak(): string {
    return `${this.name} makes a sound`;
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);
  }
  
  speak(): string {
    return `${this.name} barks`;
  }
}

迁移工具和技巧

使用 JSDoc 注释辅助迁移

在迁移过程中,可以使用 JSDoc 注释为 JavaScript 代码添加类型信息:

typescript
// JavaScript 文件,使用 JSDoc
/**
 * @param {string} name
 * @param {number} age
 * @returns {Object}
 */
function createUser(name, age) {
  return {
    name,
    age,
    isActive: true
  };
}

启用 checkJs: true 后,TypeScript 会检查 JSDoc 注释中的类型。

使用类型断言处理遗留代码

在迁移过程中,对于暂时无法确定类型的代码,可以使用类型断言:

typescript
// 处理遗留的 JavaScript 代码
const legacyData = getLegacyData() as UserData;

// 或者使用 unknown 类型
const legacyData: unknown = getLegacyData();
if (isUserData(legacyData)) {
  // 使用类型守卫
  processUserData(legacyData);
}

使用 @ts-ignore 和 @ts-expect-error

在迁移过程中,可以临时使用注释忽略类型错误:

typescript
// @ts-ignore:忽略下一行的类型错误(不推荐)
const result = someFunction();

// @ts-expect-error:期望下一行有类型错误(更推荐)
// @ts-expect-error - 这个函数将在下个版本修复
const result = someFunction();

注意

@ts-ignore@ts-expect-error 应该只是临时使用,最终目标是要修复所有类型错误。

迁移检查清单

阶段 1:准备阶段

  • [ ] 安装 TypeScript 和相关依赖
  • [ ] 创建 tsconfig.json 配置文件
  • [ ] 配置构建脚本
  • [ ] 确保现有代码可以正常运行

阶段 2:基础迁移

  • [ ] 为新文件使用 .ts 扩展名
  • [ ] 为函数参数和返回值添加类型
  • [ ] 为变量添加类型注解(必要时)
  • [ ] 安装第三方库的类型定义

阶段 3:类型完善

  • [ ] 定义接口和类型别名
  • [ ] 为对象和数组添加类型
  • [ ] 处理可选属性和默认值
  • [ ] 添加泛型类型(如需要)

阶段 4:严格模式

  • [ ] 启用 noImplicitAny
  • [ ] 启用 strictNullChecks
  • [ ] 启用 strictFunctionTypes
  • [ ] 最终启用完整 strict 模式

阶段 5:优化和维护

  • [ ] 移除所有 @ts-ignore 注释
  • [ ] 移除不必要的类型断言
  • [ ] 优化类型定义
  • [ ] 更新文档和注释

常见问题和解决方案

问题 1:大量类型错误

症状:重命名文件后出现大量类型错误

解决方案

  1. 先不重命名文件,保持 .js 扩展名
  2. 逐步添加类型注解
  3. 使用 // @ts-check 在 JavaScript 文件中启用类型检查
  4. 逐步修复错误,不要一次性修复所有错误

问题 2:第三方库没有类型定义

症状:导入第三方库时出现类型错误

解决方案

typescript
// 方案 1:安装 @types 包
npm install --save-dev @types/library-name

// 方案 2:创建声明文件
// types/library-name.d.ts
declare module 'library-name' {
  export function someFunction(): void;
  export const someConstant: string;
}

// 方案 3:使用类型断言
import * as lib from 'library-name';
const typedLib = lib as any;

问题 3:动态导入和 require

症状:使用 require 或动态导入时类型检查失败

解决方案

typescript
// 方案 1:使用 import
import * as express from 'express';

// 方案 2:使用 require 的类型定义
const express = require('express') as typeof import('express');

// 方案 3:动态导入
const loadModule = async () => {
  const module = await import('./module');
  return module;
};

问题 4:全局变量和 window 对象

症状:访问全局变量时出现类型错误

解决方案

typescript
// 方案 1:扩展 Window 接口
interface Window {
  myGlobal: string;
}

window.myGlobal = 'value';

// 方案 2:声明全局变量
declare global {
  const MY_GLOBAL: string;
}

// 方案 3:使用类型断言
const value = (window as any).myGlobal;

最佳实践

实践 1:逐步迁移,不要急于求成

提示

迁移是一个渐进的过程,不要试图一次性完成所有工作。优先迁移核心业务逻辑,工具脚本可以保持 JavaScript。

实践 2:优先为新代码添加类型

新功能和新文件应该直接使用 TypeScript,这样可以:

  • 避免技术债务积累
  • 逐步提高代码库的类型覆盖率
  • 团队可以逐步学习 TypeScript

实践 3:使用类型而非类型断言

typescript
// ❌ 不推荐:过度使用类型断言
const user = data as User;

// ✅ 推荐:使用类型守卫
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'name' in data &&
    'age' in data
  );
}

if (isUser(data)) {
  // TypeScript 知道 data 是 User 类型
  console.log(data.name);
}

实践 4:利用类型推断

不需要为所有变量添加类型注解,让 TypeScript 推断类型:

typescript
// ✅ 推荐:利用类型推断
const name = 'TypeScript'; // 推断为 string
const count = 42; // 推断为 number

// 只在必要时添加类型注解
function process(data: unknown): ProcessedData {
  // 函数返回值需要明确类型
}

实践 5:保持配置灵活

在迁移初期,保持配置灵活,允许 JavaScript 和 TypeScript 共存:

json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false
  }
}

注意事项

注意

  1. 不要过度使用 any:使用 any 会失去类型检查的优势,应该尽量避免
  2. 逐步启用严格模式:一次性启用所有严格检查可能导致大量错误,应该逐步启用
  3. 保持代码可运行:迁移过程中确保代码始终可以运行,不要破坏现有功能
  4. 团队协作:确保团队成员了解迁移策略和进度

提示

  • 迁移是一个持续的过程,不要期望一次性完成
  • 优先迁移核心业务逻辑,工具脚本可以保持 JavaScript
  • 利用 TypeScript 的类型推断,不需要为所有代码添加类型
  • 定期检查类型覆盖率,逐步提高代码质量

重要

  • 类型错误不会阻止运行:TypeScript 的类型错误只在编译时检查,不会影响 JavaScript 运行
  • 保持向后兼容:迁移过程中确保 API 和接口保持向后兼容
  • 测试覆盖:迁移过程中保持充分的测试覆盖,确保功能正常

迁移示例:完整案例

案例:迁移一个简单的用户管理模块

步骤 1:原始 JavaScript 代码

javascript
// user.js
function createUser(name, age, email) {
  return {
    name: name || 'Unknown',
    age: age || 0,
    email: email || '',
    isActive: true,
    createdAt: new Date()
  };
}

function getUserInfo(user) {
  return `${user.name} (${user.age}) - ${user.email}`;
}

function activateUser(user) {
  user.isActive = true;
  return user;
}

module.exports = {
  createUser,
  getUserInfo,
  activateUser
};

步骤 2:添加类型定义

typescript
// user.ts
interface User {
  name: string;
  age: number;
  email: string;
  isActive: boolean;
  createdAt: Date;
}

function createUser(
  name: string = 'Unknown',
  age: number = 0,
  email: string = ''
): User {
  return {
    name,
    age,
    email,
    isActive: true,
    createdAt: new Date()
  };
}

function getUserInfo(user: User): string {
  return `${user.name} (${user.age}) - ${user.email}`;
}

function activateUser(user: User): User {
  return {
    ...user,
    isActive: true
  };
}

export { createUser, getUserInfo, activateUser };
export type { User };

步骤 3:使用示例

typescript
// main.ts
import { createUser, getUserInfo, activateUser, type User } from './user';

const user = createUser('Alice', 30, 'alice@example.com');
console.log(getUserInfo(user));

const activatedUser = activateUser(user);
console.log(activatedUser.isActive); // true

相关链接

基于 VitePress 构建