集成 TypeScript

TypeScript 是单体仓库中的优秀工具,允许团队安全地为 JavaScript 代码添加类型。虽然设置有一些复杂性,但本指南将引导你了解大多数用例的 TypeScript 设置的重要部分。

本指南假设你使用的是最新版本的 TypeScript,并使用了一些仅在这些版本中可用的功能。如果你无法使用这些版本的功能,可能需要调整本页面上的指导。

共享 tsconfig.json

你希望在 TypeScript 配置中构建一致性,以便整个仓库可以使用优秀的默认设置,你的团队成员在工作区中编写代码时能知道期望什么。

TypeScript 的 tsconfig.json 设置了 TypeScript 编译器的配置,并具有 extends 键,你将使用它在工作区之间共享配置。

本指南将使用 create-turbo 作为示例。

Terminal
npx create-turbo@latest

使用基础 tsconfig 文件

在 packages/typescript-config 中,你有几个 json 文件,代表了你可能想要在各种包中配置 TypeScript 的不同方式。base.json 文件被工作区中的每个其他 tsconfig.json 扩展,如下所示:

./packages/typescript-config/base.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "module": "NodeNext"
  }
}
→

tsconfig 选项参考

创建包的其余部分

此包中的其他 tsconfig 文件使用 extends 键以基础配置开始,并为特定类型的项目进行自定义,如 Next.js(nextjs.json)和 React 库(react-library.json)。

在 package.json 中,命名包以便在工作区的其余部分引用:

packages/typescript-config/package.json
{
  "name": "@repo/typescript-config"
}

构建 TypeScript 包

使用配置包

首先,将 @repo/typescript-config 包安装到你的包中:

./apps/web/package.json
{
  "devDependencies": {
     "@repo/typescript-config": "*",
     "typescript": "latest",
  }
}

然后,从 @repo/typescript-config 包扩展包的 tsconfig.json。在这个例子中,web 包是一个 Next.js 应用程序:

./apps/web/tsconfig.json
{
  "extends": "@repo/typescript-config/nextjs.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

创建包的入口点

首先,确保你的代码用 tsc 编译,这样会有一个 dist 目录。你需要一个 build 脚本和一个 dev 脚本:

./packages/ui/package.json
{
  "scripts": {
    "dev": "tsc --watch",
    "build": "tsc"
  }
}

然后,在 package.json 中设置包的入口点,以便其他包可以使用编译后的代码:

./packages/ui/package.json
{
  "exports": {
    "./*": {
      "types": "./src/*.ts",
      "default": "./dist/*.js"
    }
  }
}

这样设置 exports 有几个优点:

  • 使用 types 字段允许 tsserver 使用 src 中的代码作为代码类型的真实来源。你的编辑器将始终与代码中的最新接口保持同步。
  • 你可以快速向包添加新的入口点,而无需创建危险的桶文件。
  • 你将在编辑器中收到跨包边界的导入自动导入建议。

如果你正在发布包,你不能在 types 中使用对源代码的引用,因为只有编译后的代码会发布到 npm。你需要生成并引用声明文件和源映射。

代码库的检查

要使用 TypeScript 作为代码检查器,你可以使用 Turborepo 的缓存和并行化快速检查工作区中的类型。

首先,向你想要检查类型的任何包添加 check-types 脚本:

./apps/web/package.json
{
  "scripts": {
    "check-types": "tsc --noEmit"
  }
}

然后,在 turbo.json 中创建一个 check-types 任务。根据配置任务指南,我们可以使用传输节点让任务并行运行,同时尊重来自其他包的源代码更改:

Turborepo logo
./turbo.json
{
  "tasks": {
    "topo": {
      "dependsOn": ["^topo"]
    },
    "check-types": {
      "dependsOn": ["topo"]
    }
  }
}

然后,使用 turbo check-types 运行你的任务。

最佳实践

使用 tsc 编译你的包

对于内部包,我们建议尽可能使用 tsc 编译你的 TypeScript 库。虽然你可以使用打包器,但这并不是必需的,并且会给你的构建过程增加额外的复杂性。此外,打包库可能会在代码到达应用程序的打包器之前篡改代码,导致难以调试的问题。

启用跨包边界的转到定义

"转到定义"是一种编辑器功能,可以通过点击或热键快速导航到符号(如变量或函数)的原始声明或定义。一旦 TypeScript 配置正确,你可以轻松地在内部包之间导航。

即时包

来自即时包的导出将自动带你到原始的 TypeScript 源代码。转到定义将按预期工作。

编译包

来自编译包的导出需要使用 declaration 和 declarationMap 配置才能使转到定义工作。在为包启用这两个配置后,使用 tsc 编译包,并打开输出目录以查找声明文件和源映射。

button.js
button.d.ts
button.d.ts.map

有了这两个文件,你的编辑器现在将导航到原始源代码。

使用 Node.js 子路径导入而不是 TypeScript 编译器 paths

可以使用 TypeScript 编译器的 paths 选项 在包中创建绝对导入,但这些路径在使用即时包时可能导致编译失败。从 TypeScript 5.4 开始,你可以使用 Node.js 子路径导入作为更健壮的解决方案。

即时包

在即时包中,imports 必须针对包中的源代码,因为不会创建 dist 等构建输出。

./packages/ui/package.json
{
  "imports": {
    "#*": "./src/*"
  }
}

编译包

在编译包中,imports 针对包的构建输出。

./packages/ui/package.json
{
  "imports": {
    "#*": "./dist/*"
  }
}

你可能不需要在项目根目录中的 tsconfig.json 文件

如构建你的仓库指南中所述,你希望将工具中的每个包视为自己的单元。这意味着每个包都应该有自己的 tsconfig.json 来使用,而不是引用项目根目录中的 tsconfig.json。遵循这种做法将使 Turborepo 更容易缓存你的类型检查任务,简化你的配置。

你可能想要在工作区根目录中有一个 tsconfig.json 的唯一情况是为不在包中的 TypeScript 文件设置配置。例如,如果你有一个用 TypeScript 编写的需要从根目录运行的脚本,则可能需要为该文件提供 tsconfig.json。

然而,也不建议这样做,因为工作区根目录中的任何更改都会导致所有任务错过缓存。相反,将这些脚本移动到仓库中的不同目录。

你可能不需要 TypeScript 项目引用

我们不建议使用 TypeScript 项目引用,因为它们在你的工作区中引入了另一个配置点和另一个缓存层。这两者都可能在你的仓库中引起问题,几乎没有好处,所以我们建议在使用 Turborepo 时避免使用它们。

限制

你的编辑器不会使用包的 TypeScript 版本

tsserver 无法在代码编辑器中为不同的包使用不同的 TypeScript 版本。相反,它将发现一个特定版本并在所有地方使用它。

这可能会导致在编辑器中显示的代码检查错误与运行 tsc 脚本检查类型时的错误之间存在差异。如果这对你来说是个问题,请考虑保持 TypeScript 依赖项在同一版本。