迁移到 TypeScript - 高级指南

2025-05-25

迁移到 TypeScript - 高级指南

大约一年前,我写了一篇关于如何在 Node.js 上从 JavaScript 迁移到 TypeScript 的指南,浏览量超过 7000 次。当时我对 JavaScript 和 TypeScript 都了解不多,可能过于关注某些工具,而忽略了全局。最大的问题是,我没有提供迁移大型项目的解决方案,因为大型项目显然不可能在短时间内重写所有内容,因此我迫切地想分享我学到的关于如何迁移到 TypeScript 的最新、最棒的经验。

将你庞大的数千个文件的 Mono Repo 项目迁移到 TypeScript 的整个过程比你想象的要简单。以下是 3 个主要步骤。

注意:本文假设您了解 TypeScript 的基础知识并使用Visual Studio Code,如果不了解,某些细节可能不适用。

本指南的相关代码:https://github.com/llldar/migrate-to-typescript-the-advance-guide

开始输入

经过10个小时的调试console.log,你终于修复了那个Cannot read property 'x' of undefined错误,结果发现它是由于调用了某个可能是 的方法undefined:真是意外!你暗自发誓要把整个项目迁移到 TypeScript。但是,当你看到libutilcomponents文件夹以及里面成千上万的 JavaScript 文件时,你心想:“也许以后,也许等我有时间的时候再说吧。” 当然,那一天永远不会到来,因为你总是有“很酷的新功能”要添加到应用中,而且客户无论如何也不会为 TypeScript 支付更多费用。

现在,如果我告诉您可以逐步迁移到 TypeScript 并立即开始从中受益,您会怎么做

添加魔法d.ts

d.ts文件是来自 typescript 的类型声明文件,它们所做的只是声明代码中使用的各种类型的对象和函数,并不包含任何实际逻辑。

现在考虑您正在编写一个消息应用程序:

假设您有一个名为的常量user和其中的一些数组user.js

const user = {
  id: 1234,
  firstname: 'Bruce',
  lastname: 'Wayne',
  status: 'online',
};

const users = [user];

const onlineUsers = users.filter((u) => u.status === 'online');

console.log(
  onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`)
);
Enter fullscreen mode Exit fullscreen mode

对应的user.d.ts

export interface User {
  id: number;
  firstname: string;
  lastname: string;
  status: 'online' | 'offline';
}
Enter fullscreen mode Exit fullscreen mode

然后你就有了这个名为sendMessageinside的函数message.js

function sendMessage(from, to, message)
Enter fullscreen mode Exit fullscreen mode

中对应的界面message.d.ts应该是这样的:

type sendMessage = (from: string, to: string, message: string) => boolean
Enter fullscreen mode Exit fullscreen mode

然而,我们sendMessage可能没有那么简单,也许我们可以使用一些更复杂的类型作为参数,或者它可能是一个异步函数

对于复杂类型,您可以使用它import来帮助解决问题,保持类型清晰并避免重复。

import { User } from './models/user';
type Message = {
  content: string;
  createAt: Date;
  likes: number;
}
interface MessageResult {
  ok: boolean;
  statusCode: number;
  json: () => Promise<any>;
  text: () => Promise<string>;
}
type sendMessage = (from: User, to: User, message: Message) => Promise<MessageResult>
Enter fullscreen mode Exit fullscreen mode

注意:我typeinterface这里使用了它们来向您展示如何使用它们,您应该在您的项目中坚持使用其中一种。

连接类型

现在您有了类型,它们如何与您的js文件一起工作?

一般有2种方法:

Jsdoc typedef 导入

假设它们位于同一文件夹中,则在您的文件user.d.ts中添加以下注释:user.js

/**
 * @typedef {import('./user').User} User
 */

/**
 * @type {User}
 */
const user = {
  id: 1234,
  firstname: 'Bruce',
  lastname: 'Wayne',
  status: 'online',
};

/**
 * @type {User[]}
 */
const users = [];

// onlineUser would automatically infer its type to be User[]
const onlineUsers = users.filter((u) => u.status === 'online');

console.log(
  onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`)
);
Enter fullscreen mode Exit fullscreen mode

要正确使用此方法,您需要将importexport保留在文件中d.ts。否则,您最终会得到anytype,这绝对不是您想要的。

三斜线指令

当您在某些情况下import无法使用三斜线指令时,它是 TypeScript 中的“好方法” 。import

eslint config file注意:您可能需要在处理时添加以下内容triple slash directive以避免出现 eslint 错误。

{
  "rules": {
    "spaced-comment": [
      "error",
      "always",
      {
        "line": {
          "markers": ["/"]
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

对于消息功能,将以下内容添加到您的message.js文件中,假设message.jsmessage.d.ts位于同一文件夹中

/// <reference path="./models/user.d.ts" /> (add this only if you use user type)
/// <reference path="./message.d.ts" />
Enter fullscreen mode Exit fullscreen mode

然后在函数jsDoc上方添加注释sendMessage

/**
* @type {sendMessage}
*/
function sendMessage(from, to, message)
Enter fullscreen mode Exit fullscreen mode

然后您会发现sendMessage现在输入正确,并且在使用时您可以从 IDE 获得自动完成fromto以及message函数返回类型。

或者,您可以按如下方式编写它们

/**
* @param {User} from
* @param {User} to
* @param {Message} message
* @returns {MessageResult}
*/
function sendMessage(from, to, message)
Enter fullscreen mode Exit fullscreen mode

这更像是一种编写jsDoc函数签名的惯例。但肯定更冗长。

使用时,您应该从文件中triple slash directive删除import,否则不起作用,如果您必须从另一个文件导入某些内容,请使用如下方法:exportd.tstriple slash directive

type sendMessage = (
  from: import("./models/user").User,
  to: import("./models/user").User,
  message: Message
) => Promise<MessageResult>;
Enter fullscreen mode Exit fullscreen mode

所有这些背后的原因是,d.ts如果文件没有任何导入或导出,TypeScript 会将其视为环境模块声明。如果文件包含importexport,则会将其视为普通模块文件,而不是全局模块文件,因此在triple slash directive或中使用它们augmenting module definitions将不起作用。

注意:在实际项目中,请坚持使用其中一个import and exporttriple slash directive不要同时使用它们。

自动生成d.ts

如果你的 JavaScript 代码中已经有很多jsDoc注释,那么你很幸运,只需一行简单的

npx typescript src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types
Enter fullscreen mode Exit fullscreen mode

假设所有 js 文件都在src文件夹中,则输出d.ts文件将位于types文件夹中

Babel 配置(可选)

如果你的项目中已经设置了 Babel,你可能需要将其添加到你的babelrc

{
  "exclude": ["**/*.d.ts"]
}
Enter fullscreen mode Exit fullscreen mode

避免将*.d.ts文件编译成*.d.js,这没有任何意义。

现在,您应该能够从 typescript(自动完成)中受益,而无需对 js 代码进行任何配置和逻辑更改。

类型检查

在上述步骤覆盖了至少 70% 以上的代码库之后,你现在可以考虑启用类型检查,这有助于进一步消除代码库中的小错误和 bug。别担心,你仍然会使用 JavaScript 一段时间,这意味着构建过程和库都不需要进行任何更改。

您需要做的主要事情是添加jsconfig.json到您的项目中。

基本上,它是一个定义项目范围并定义您将要使用的库和工具的文件。

示例jsonconfig.json文件:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "checkJs": true,
    "lib": ["es2015", "dom"]
  },
  "baseUrl": ".",
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

这里的重点是我们需要checkJs真实,这样我们就可以对所有js文件启用类型检查。

一旦启用,预计会出现大量错误,请务必逐一修复。
错误

增量类型检查

// @ts-nocheck

在一个文件中,如果您有一些js文件希望稍后修复,您可以// @ts-nocheck在页面的顶部,而 TypeScript 编译器就会忽略该文件。

// @ts-忽略

如果你只想忽略一行而不是整个文件怎么办?使用// @ts-ignore。它只会忽略其下面的一行。

// @ts-expect-error

它类似于@ts-ignore,但更好。它允许 TypeScript 编译器在某个地方不再出现错误时发出警告,这样你就知道该删除这条注释了。

这三个标签结合起来应该可以让您以稳定的方式修复代码库中的类型检查错误。

外部库

维护良好的图书馆

如果您正在使用一个流行的库,那么很可能已经在为其输入内容DefinitelyTyped,在这种情况下,只需运行:

yarn add @types/your_lib_name --dev
Enter fullscreen mode Exit fullscreen mode

或者

npm i @types/your_lib_name --save-dev
Enter fullscreen mode Exit fullscreen mode

@注意:如果您正在为名称包含和/之类的组织库安装类型声明,@babel/core则应将其名称更改为__在中间添加 并删除@/,结果类似于babel__core

纯 Js 库

如果你使用的js库是作者 10 年前归档的,并且没有提供任何 TypeScript 类型支持,会怎么样?这种情况很可能发生,因为大多数 npm 模型仍然使用 JavaScript。添加 TypeScript 类型支持@ts-ignroe似乎不是一个好主意,因为你希望尽可能保证类型安全。

现在你需要augmenting module definitions创建一个d.ts文件(最好在types文件夹中),并向其中添加你自己的类型定义。这样,你就可以享受代码的安全类型检查了。

declare module 'some-js-lib' {
  export const sendMessage: (
    from: number,
    to: number,
    message: string
  ) => Promise<MessageResult>;
}
Enter fullscreen mode Exit fullscreen mode

完成所有这些之后,您应该有一种很好的方法来检查代码库并避免小错误。

类型检查上升

现在,你已经修复了 95% 以上的类型检查错误,并确保每个库都有相应的类型定义。你可以进行最后一步了:正式将你的代码库迁移到 TypeScript。

注意:我不会在这里介绍细节,因为我之前的文章已经介绍过了

将所有文件更改为.ts文件

现在是时候将这些d.ts文件与你的 js 文件合并了。几乎所有类型检查错误都已修复,并且所有模块都已进行类型覆盖。你所做的本质上是将require语法更改为import并将所有内容合并到一个ts文件中。鉴于你之前所做的所有工作,这个过程应该相当简单。

将 jsconfig 更改为 tsconfig

现在你需要tsconfig.json一个jsconfig.json

例子tsconfig.json

前端项目

{
  "compilerOptions": {
    "target": "es2015",
    "allowJs": false,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitThis": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "lib": ["es2020", "dom"],
    "skipLibCheck": true,
    "typeRoots": ["node_modules/@types", "src/types"],
    "baseUrl": ".",
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

后端项目

{
  "compilerOptions": {
      "sourceMap": false,
      "esModuleInterop": true,
      "allowJs": false,
      "noImplicitAny": true,
      "skipLibCheck": true,
      "allowSyntheticDefaultImports": true,
      "preserveConstEnums": true,
      "strictNullChecks": true,
      "resolveJsonModule": true,
      "moduleResolution": "node",
      "lib": ["es2018"],
      "module": "commonjs",
      "target": "es2018",
      "baseUrl": ".",
      "paths": {
          "*": ["node_modules/*", "src/types/*"]
      },
      "typeRoots": ["node_modules/@types", "src/types"],
      "outDir": "./built",
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Enter fullscreen mode Exit fullscreen mode

由于类型检查变得更加严格,因此修复此更改后的任何附加类型检查错误。

更改 CI/CD 管道和构建流程

您的代码现在需要一个构建过程来生成可运行的代码,通常将其添加到您的代码中package.json就足够了:

{
  "scripts":{
    "build": "tsc"
  }
}
Enter fullscreen mode Exit fullscreen mode

然而,对于前端项目,你通常需要 babel,你可以像这样设置你的项目:

{
  "scripts": {
    "build": "rimraf dist && tsc --emitDeclarationOnly && babel src --out-dir dist --extensions .ts,.tsx && copyfiles package.json LICENSE.md README.md ./dist"
  }
}
Enter fullscreen mode Exit fullscreen mode

现在确保在文件中更改入口点,如下所示:

{
  "main": "dist/index.js",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
}
Enter fullscreen mode Exit fullscreen mode

那么一切就都设置好了。

注意:更改dist为您实际使用的文件夹。

结束

恭喜,您的代码库现已使用 TypeScript 编写,并经过了严格的类型检查。现在您可以享受 TypeScript 的所有优势,例如自动完成、静态类型、esnext 语法和出色的可扩展性。DX 正在飞速发展,而维护成本却保持最低。项目开发不再是一个痛苦的过程,您也Cannot read property 'x' of undefined再也不会遇到类似的错误了。

替代方法:

如果你想以更“全能”的方式迁移到 TypeScript,这里有一个很棒的指南,由 airbnb 团队提供

文章来源:https://dev.to/natelindev/migrate-to-typescript-the-advance-guide-1df6
PREV
Rust 的隐藏力量
NEXT
使用 IMG 标签窃取账户 使用图像抓取观众数据 窃取账户 真实案例