Skip to content

[译] Node.js 中集成 TypeScript 入门指南 ​

原文地址:https://betterstack.com/community/guides/scaling-nodejs/nodejs-typescript

TypeScript 是 JavaScript 语法的扩展,为该语言增加了类型安全和其他功能。自 2011 年首次亮相以来,TypeScript 不断受到欢迎,并越来越多地用于对可靠性要求极高的各类项目。Deno 和 Bun 等新兴 JavaScript 运行时都内置了 TypeScript 支持,而 Node.js 却没有。因此,你需要投入额外的精力在 Node.js 运行时中集成类型检查。本文将教你如何做到这一点!

学习前提 ​

在开始本教程之前,请确保你已安装最新版本的 Node.js 和 npm。此外,由于本教程仅关注如何将 TypeScript 顺利集成到 Node.js 项目中,因此希望你对 TypeScript 有基本的了解。

步骤1:下载演示项目(可选) ​

为了说明将 TypeScript 无缝集成到 Node.js 项目中的过程,我们将使用 演示的 Node.js 应用程序 来显示当前各种加密货币和法定货币的比特币价格。虽然我们将使用这个特定项目进行演示,但你也可以将本指南中列出的步骤应用到你选择的任何其他项目中。

首先,运行下面的命令将代码仓库 clone 到你的机器上:

bash
$ git clone https://github.com/betterstack-community/btc-exchange-rates

进入新创建的 btc-exchange-rates 目录,运行下面的命令下载项目的所有依赖项:

bash
$ npm install

然后,执行以下命令启动端口为 3000 的开发服务器:

bash
$ npm run dev

一旦执行成功,你将看到如下的输入:

bash
> btc-exchange-rates@1.0.0 dev
> nodemon server.js

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
server started on port: 3000
Exchange rates cache updated

请在浏览器中访问 http://localhost:3000 查看应用程序的运行情况:

截图_20241013100222

现在你可以进入下一部分,在 Node.js 项目中安装和配置 TypeScript 编译器。

步骤2:安装并配置 TypeScript ​

现在,你已经设置好了演示应用程序,然后通过下面的命令在我们的项目中安装 TypeScript 编译器:

bash
$ npm install typescript

在本地安装 TypeScript 可以确保版本记录在项目的 package.json 文件中,这样将来 clone 项目的人就可以使用相同的 TypeScript 版本,防止版本之间可能出现的破坏性更改。

安装后,项目中就会有 tsc 命令,可以通过 npx 访问,如下所示:

bash
$ npx tsc --version
bash
Version 5.2.2

根据你学习本教程的时间,你可能会看到不同版本的 TypeScript。通常,每三个月会发布一个新版本。

在开始编译成 JavaScript 源文件之前,你需要设置一个 tsconfig.json 配置文件。如果没有它,当你尝试编译项目时,TypeScript 编译器就会出错。虽然可以使用命令行标志,但配置文件更易于管理。

截图_20241013100816

随后在项目根目录下创建一个 tsconfig.json 文件:

bash
$ code tsconfig.json

在文本编辑器中打开文件后,粘贴以下内容:

json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

TypeScript 提供了大量 配置选项,可帮助你指定应包含哪些文件以及编译器的严格程度。以下是对上述基本配置的解释:

  • extends:提供了一种从另一个配置文件继承的方式。在本示例中,我们使用的是 Node v20 的基本配置,但你也可以根据自己的 Node.js 版本使用更合适的 基本配置。
  • include:指定程序中应包含哪些文件。
  • exclude:指定编译时应省略的文件或目录。

另一个未在此处显示的关键属性是 compilerOptions。它是 TypeScript 配置的主要部分,涵盖了语言的工作方式。当上面省略时,默认使用基本配置中指定的 compilerOptions 或 TypeScript 编译器默认值:

json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Node 20",
  "_version": "20.1.0",

  "compilerOptions": {
    "lib": ["es2023"],
    "module": "node16",
    "target": "es2022",

    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node16"
  }
}

上面提到的基本配置是以 NPM 软件包 的形式提供的,因此需要安装后才能生效:

bash
$ npm install --save-dev @tsconfig/node20

安装基础配置后,在项目根目录下执行以下命令:

bash
$ npx tsc
bash
error TS18003: No inputs were found in config file '/home/ayo/dev/demo/btc/tsconfig.json'. Specified 'include' paths were '["src/**/*"]' and 'exclude' paths were '["node_modules"]'.


Found 1 error.

由于 src 目录中没有 .ts 文件,你会遇到上述错误。要解决这个问题,可以调整 tsconfig.json 配置,通过 allowJs 选项识别 JavaScript 文件。这种方法可以帮助你逐步将 JavaScript 项目过渡到 TypeScript。你还应使用 outDir 选项指定已编译 JavaScript 文件的输出目录:

json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "allowJs": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

保存文件后,再次运行 TypeScript 编译器。你会发现没有输出,这意味着编译成功。

bash
$ npx tsc

现在你应该能在项目根目录下看到一个包含已编译 server.js 文件的 dist 目录。

bash
$ ls dist
text
server.js

WARNING

译者注:windows 系统可以打开 PowerShell 来运行 ls 命令

现在,你可以修改 nodemon.json 配置,以运行编译后的文件,而不是源文件:

json
{
  "watch": ["src"],
  "ext": ".js",
  "ignore": [],
  "exec": "node dist/server.js"
}

通过这些步骤,你就成功地将 TypeScript 集成到了 Node.js 项目中!

步骤3:类型检查 JavaScript 文件(可选) ​

你现在已设置好同时编译 JavaScript 和 TypeScript 文件,但默认情况下不会对 .js 文件执行类型检查。如果你正在将 Node.js 项目过渡到 TypeScript,并希望利用类型检查而不一次性将所有现有 .js 文件转换为 .ts,你可以使用 checkJs 编译器选项启用前者的类型检查:

json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

这里需要提醒的是:在一个大型项目中启用 checkJs 可能会导致错误泛滥,而在不过渡到 .ts 文件的情况下处理这些错误可能不是最有效的方法。

即使在我们这个不到 100 行的单个文件项目中,仅启用该选项就会产生 20 个错误:

截图_20241013103102

你可以选择在每个文件的开头使用 @ts-check 注释进行类型检查,而不是全局启用 checkJs:

js
// @ts-check
import express from 'express';
import path from 'node:path';
. . .

相反,如果启用了 checkJs,但希望将特定文件排除在类型检查之外,可使用 @ts-nocheck 注释:

js
// @ts-nocheck
import express from 'express';
import path from 'node:path';
. . .

这对于暂时绕过有问题的文件非常方便,因为你可能没有能力立即处理这些文件。

为了实现更精细的控制,TypeScript 提供了 @ts-ignore 和 @ts-expect-error 注释。它们允许你逐行控制类型检查:

ts
// @ts-expect-error
const app = express();

// or

// @ts-ignore
const app = express();

这些特殊注释在 JavaScript(启用了 allowJs)和 TypeScript 文件中都有效。鉴于我们的演示应用程序非常简单,我们将不使用 checkJs 或本节讨论的特殊注释。相反,我们将直接过渡到 TypeScript,并处理过程中出现的任何类型错误。

步骤4:迁移 JavaScript 文件为 TypeScript ​

从 JavaScript 过渡到 TypeScript 就像将文件扩展名从 .js 改为 .ts 一样简单。由于每个有效的 JavaScript 程序都是有效的 TypeScript 程序,因此只需进行简单的更改,你就可以开始使用 TypeScript 的功能。

bash
$ mv src/server.js src/server.ts

然后,按如下方式更改 nodemon.json 文件中的 ext 字段:

json
{
  "watch": ["src"],
  "ext": ".js,.ts",
  "ignore": [],
  "exec": "tsc && node dist/server.js"
}

对于 Node.js 项目,安装 @types/node 软件包至关重要。该包提供了内置 Node.js API 的类型定义,TypeScript 编译器会自动检测到这些类型。

bash
$ npm install --save-dev @types/node

此时,你可以运行 TypeScript 编译器,看看是否有任何错误:

bash
$ npx tsc

截图_20241013103818

现在,编译器可以确定内置 Node.js API 的特定类型,错误从 20 个减少到 7 个。剩下的错误大多是因为编译器无法确定某些第三方库的类型。

TypeScript 在遇到没有类型定义的 JavaScript 库时,会默认使用 any 类型。然而,在更严格的配置中不允许隐式使用 any(参见此处),从而导致这些错误。在下一节中,你将发现解决这个问题的一些策略。

一旦完成将 JavaScript 源文件迁移到 TypeScript,你就可以从 tsconfig.json 中移除 allowJs 编译器选项。

步骤5:修复第三方库引起的类型错误 ​

虽然 TypeScript 功能强大,但它在与 JavaScript 编写的第三方库配合时经常会遇到挑战。它依赖于类型信息来确保类型安全,而 JavaScript 项目无法在开箱即用时提供这些信息。让我们在本节中讨论这些挑战。

理解问题 ​

当 TypeScript 遇到 JavaScript 编写的库时,它会默认整个库使用 any 类型。这是一种万能类型,基本上可以绕过 TypeScript 的类型检查。

noImplicitAny 编译器选项有助于缓解这一问题。启用该选项后,TypeScript 将在无法推断类型时抛出错误,而不是默认为任何类型。该选项是 严格配置 的一部分,可确保更严格的类型检查。

解决方案:类型声明 ​

为了帮助 TypeScript 理解 JavaScript 库,作者可以在其软件包中提供类型声明文件(以 .d.ts 结尾)。这些文件描述了库的结构,使 TypeScript 能够检查类型,并在编辑器中提供更好的自动完成功能。

有些库(如 axios)在其主包中包含了类型声明,但许多其他库(如 express 和 morgan)则没有。对于这些库,TypeScript 社区通常会在 NPM 的 @types 作用域 下单独创建并发布它们的类型声明。这些类型声明都来自社区,可以在 DefinitelyTyped 仓库 中找到。

修复错误 ​

下面是上一步编译程序时出现的前两个错误:

bash
src/server.ts:1:21 - error TS7016: Could not find a declaration file for module 'express'. '/home/ayo/dev/betterstack/demo/btc-exchange-rates/node_modules/express/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/express` if it exists or add a new declaration (.d.ts) file containing `declare module 'express';`

1 import express from 'express';
                      ~~~~~~~~~

src/server.ts:4:20 - error TS7016: Could not find a declaration file for module 'morgan'. '/home/ayo/dev/betterstack/demo/btc-exchange-rates/node_modules/morgan/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/morgan` if it exists or add a new declaration (.d.ts) file containing `declare module 'morgan';`

4 import morgan from 'morgan';
                     ~~~~~~~~

. . .

正如错误信息所示,通常可以通过在 @types 作用域中安装适当的类型声明来解决类型问题。对于当前与 express 和 morgan 有关的错误,请运行以下命令:

bash
$ npm install --save-dev @types/express @types/morgan

安装类型声明后,重新运行 TypeScript 编译器。现在所有的 implicit any 错误都应该消失了,只剩下一个错误:

bash
$ npx tsc
bash
src/server.ts:76:27 - error TS18046: 'data' is of type 'unknown'.

76       lastUpdated: format(data.timestamp, 'LLL dd, yyyy hh:mm:ss a O'),
                             ~~~~

Found 2 errors in the same file, starting at: src/server.ts:31

从现在起,如果你尝试使用库的 API 进行非法操作,编译器会立即提醒你注意问题所在:

ts
// 尝试使用一个不存在的方法
app.misuse(morganMiddleware);
bash
$ npx tsc
bash
src/server.ts:23:5 - error TS2339: Property 'misuse' does not exist on type 'Express'.

23 app.misuse(morganMiddleware);
       ~~~~~~

如果你使用的库不太流行,在 @types 作用域中没有可用的类型声明,你可能需要为该库编写自定义类型声明文件。请参阅 TypeScript 手册,了解如何编写。

步骤6:修复其他类型错误 ​

在上一节中,我们成功修复了所有因 JavaScript 库中缺少类型信息而导致的 implicit any 错误,但还有一个错误需要解决:

bash
src/server.ts:74:35 - error TS2571: Object is of type 'unknown'.

74       lastUpdated: dateFns.format(data.timestamp, 'LLL dd, yyyy hh:mm:ss a O'),
                                     ~~~~
Found 1 error in src/server.ts:74

下划线 data 实体包含从 Coin Gecko API 接收到的汇率数据以及最后更新的日期。这个错误表明,TypeScript 无法确定 data 对象的类型,因此为其分配了 unknown 类型。

any 允许你对变量做任何事情,而 unknown 则更具限制性:如果不首先断言或缩小其类型,你就无法做任何事情。要解决这个错误,你需要通过创建自定义类型来告知 TypeScript 这个实体的形状。

继续将下面突出显示的行添加到你的 src/server.ts 文件中:

ts
. . .

app.set('views', path.join(__dirname, '..', 'views'));

type Currency = {
  name: string;
  unit: string;
  value: number;
  type: string;
};

type Rates = {
  rates: {
    [key: string]: Currency;
  };
};

type ExchangeRateResult = {
  timestamp: Date;
  data: Rates;
};

. . .

在这里,ExchangeRateResult 描述了 data 实体的形状,看起来像这样:

json
{
    "timestamp": "2023-10-01T04:20:18.357Z",
    "data": {
        "rates": {
            "aed": {
                "name": "United Arab Emirates Dirham",
                "type": "fiat",
                "unit": "DH",
                "value": 99314.944
            },
            "ars": {
                "name": "Argentine Peso",
                "type": "fiat",
                "unit": "$",
                "value": 9464154.224
            }
        }
    }
}

下一步是用自定义类型注释函数的返回类型,让编译器了解你正在处理的数据的形状:

ts
. . .
async function getExchangeRates(): Promise<Rates> {
  // ... function body ...
}

async function refreshExchangeRates(): Promise<ExchangeRateResult> {
  // ... function body ...
}
. . .

最后,在根路由中指定 data 对象的类型,如下图所示,以反映它可以是 ExchangeRateResult、null 或 undefined 如果在缓存中找不到。

ts
. . .
app.get('/', async (req, res, next) => {
  try {
    let data: ExchangeRateResult | undefined = appCache.get('exchangeRates');

    . . .
  } catch (err) {
    . . .
  }
});
. . .

完成这些更改后,重新运行 TypeScript 编译器。有了额外提供的类型信息,TypeScript 编译代码时应该不会出现任何错误。

bash
$ npx tsc

通过这些步骤,你已经解决了错误,并使你的代码库更加健壮。TypeScript 的类型系统与自定义类型定义相结合,可确保你的代码更具可预测性、更易于理解,并且不易出现运行时错误。

步骤7:简化开发流程 ​

在 Node.js 环境中使用 TypeScript 时,由于需要进行类型检查和转译,开发工作流程有时会感觉很繁琐。不过,有了正确的工具和配置,你就可以简化这一过程,从而更专注于编码。

例如,你可以调整 nodemon.json 配置,将 TypeScript 编译和 Node.js 执行合并为一条命令:

json
{
  "watch": ["src"],
  "ext": ".js,.ts",
  "ignore": [],
  "exec": "tsc && node dist/server.js"
}

通过这种设置,每次对 src 目录中的 .ts 文件进行修改时,Nodemon 都会首先运行 TypeScript 编译器,然后使用 Node.js 执行编译后的 JavaScript。

虽然上述设置有效,但由于需要单独的编译步骤,因此仍存在一些开销。tsx 软件包(TypeScript Execute 的缩写)利用以速度著称的 esbuild 打包器 提供了更快的替代方案。它允许你直接运行 TypeScript 文件,而无需类型检查。

请通过下面的命令将它安装到你的项目中:

bash
$ npm install tsx --save-dev

然后,按 Ctrl-C 键退出当前的 nodemon 进程,再用 tsx 直接执行 src/server.ts 文件:

bash
$ npx tsx src/server.ts
bash
server started on port: 3000
Exchange rates cache updated

要在文件更改时自动重新启动,请使用内置的 watch 模式:

bash
$ npx tsx watch src/server.ts

tsx 的主要缺点是不执行类型检查。为了确保代码的类型安全,你可以在一个单独的终端中使用 --noEmit 标志,以观察模式运行 TypeScript 编译器。这将在不产生任何输出文件的情况下检查类型错误:

bash
$ npx tsc --watch --noEmit

通过这种设置,当出现类型错误时,你会立即得到反馈,但这不会影响你的开发速度,因为程序会继续编译。你可以忽略这些错误,直到你准备好将工作单元提交到源代码控制中。

步骤8:使用 ESLint 检查 TypeScript ​

检查(Linting)是开发过程中的一个重要部分,因为它可以捕捉潜在错误并强制执行一致的编码风格,从而帮助保持代码质量。对于 TypeScript 项目,推荐使用 ESLint,尤其是在 TSLint 过时之后。

首先安装 ESLint 和 TypeScript 的必要插件:

bash
$ npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev

然后在项目根目录下创建 .eslintrc.json 文件并如下更新文件内容:

json
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2024,
    "sourceType": "module",
    "project": "tsconfig.json"
  },
  "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
  "env": {
    "node": true
  },
  "rules": {},
  "ignorePatterns": ["dist"]
}

此配置会设置 ESLint 来解析 TypeScript 文件,并为 TypeScript 应用一组推荐的检查规则。在此阶段,只要安装了相关的 ESLint 插件,你就能在编辑器中看到检查信息。

你还可以创建一个可在命令行中执行的 lint 脚本:

json
"scripts": {
  "dev": "nodemon",
  "lint": "eslint . --fix"
}

现在运行 npm run lint 将对项目进行检查,并自动修复任何可自动修复的问题。

bash
$ npm run lint
bash
/home/ayo/dev/betterstack/demo/btc-exchange-rates/src/server.ts
   80:31  error  'next' is defined but never used             @typescript-eslint/no-unused-vars
  101:7   error  'server' is assigned a value but never used  @typescript-eslint/no-unused-vars

✖ 2 problems (2 errors, 0 warnings)

虽然推荐的规则集是一个很好的起点,但你可能需要对它们进行定制,以满足你的项目需求。例如,如果你想禁用警告未使用变量的规则,可以更新 .eslintrc.json 中的规则部分,如下所示:

json
. . .
"rules": {
  "@typescript-eslint/no-unused-vars": "off"
}
. . .

请查看 ESLint 文档 和 typescript-eslint 仓库,了解有关为 JavaScript 和 TypeScript 配置 ESLint 的更多信息。

步骤9:使用 Prettier 格式 TypeScript 代码 ​

Prettier 是一种广泛使用的代码格式化工具,支持包括 TypeScript 在内的多种语言。将 Prettier 与 ESLint 集成可确保代码中潜在错误被检查出来,并保持格式一致。以下是如何为你的 TypeScript 项目设置 Prettier:

bash
$ npm install --save-dev prettier

然后,创建一个 Prettier 配置并更新其内容,如下图所示:

json
{
  "printWidth": 80,
  "singleQuote": true,
  "trailingComma": "es5",
  "bracketSpacing": true
}

现在你可以运行 Prettier 去格式化 TypeScript 文件:

bash
$ npx prettier --write src/server.ts
bash
src/server.ts 134ms

为了确保 Prettier 的格式化不会与 ESLint 的检查规则冲突,你需要集成以下软件包:

bash
$ npm install --save-dev eslint-config-prettier eslint-plugin-prettier

然后如下修改 .eslintrc.json 文件的内容:

json
"extends": [
  "eslint:recommended",
  "plugin:@typescript-eslint/recommended",
  "plugin:prettier/recommended"
]

plugin:prettier/recommended 配置做了两件事:

  1. 它添加了 prettier 插件,将 Prettier 作为 ESLint 规则运行。
  2. 它扩展了 eslint-config-prettier 配置,关闭了所有可能与 Prettier 冲突的 ESLint 规则。

在此设置下,使用 --fix 选项运行 ESLint 也会修复 Prettier 格式化问题。如果你的编辑器集成了在保存时自动修复 ESLint 问题的功能,这一点尤其有用。

步骤10:使用 Visual Studio Code 调试 TypeScript 代码 ​

调试是应用程序开发过程中的一个重要环节,而在使用 TypeScript 时,获得无缝的调试体验至关重要。Visual Studio Code 为 TypeScript 提供了集成调试环境,让你可以直接在编辑器中调试代码。

在调试 TypeScript 代码之前,你需要生成源映射。更新你的 tsconfig.json,加入 sourceMap 选项,如下所示:

json
{
  "extends": "@tsconfig/node20/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

完成后,为 TypeScript 设置一个启动配置文件,如下所示:

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

此配置会告诉 VS Code:

  1. 使用 Node.js 调试器。
  2. 在启动调试器之前编译 TypeScript 代码。
  3. 调试位于 src 目录中的 server.ts 文件。
  4. 在 dist 目录中查找已编译的 JavaScript 文件。

有了此配置,你就可以在 Visual Studio Code 中按 F5 键开始调试。这将编译 TypeScript 代码、生成源映射(source maps)并启动调试器。确保没有其他服务器实例在运行,否则可能会出现 EADDRINUSE 错误。

启用源映射并启动调试器后,你将调试原始的 TypeScript 代码,而不是转译的 JavaScript。这意味着当你点击一个断点时,你将在编辑器中看到你的 TypeScript 代码,变量值、调用堆栈和其他调试信息将与 TypeScript 代码相对应。

截图_20241013160349

总结 ​

一开始,将 Node.js 应用程序迁移到 TypeScript 可能会让人望而生畏,但如果使用正确的工具和策略,迁移过程就会变得易于管理,而且会让你受益匪浅。TypeScript 提供了一个强大的类型系统,可以在编译时捕捉潜在的错误,从而使你的代码库更具可维护性,更不易出现运行时错误。

通过遵循本文介绍的步骤,你可以确保你的 Node.js 项目设置正确,从而受益于 TypeScript 提供的全部功能。有了增加的类型安全性和改进的工具,你会发现你的开发过程更高效、更不易出错。GitHub 存储库中提供的 typescript 分支 展示了迁移过程的结果,因此当你在自己的项目中应用本文所学知识时,它可以作为实用参考。

感谢你的阅读,祝你编码愉快!