Turborepo logo

TypeScript

TypeScript 是 monorepo 中一个优秀的工具,它允许团队安全地向其 JavaScript 代码添加类型。虽然设置过程有些复杂,但本指南将引导你完成大多数用例中 TypeScript 设置的重要部分。

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

共享 tsconfig.json

你希望在 TypeScript 配置中构建一致性,以便你的整个仓库可以使用出色的默认设置,并且你的同伴开发人员可以知道在工作区中编写代码时会发生什么。

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

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

终端
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 中的代码作为代码类型的真实来源。 你的编辑器将始终使用代码中的最新接口进行更新。
  • 你可以快速向你的包添加新的入口点,而无需创建危险的 barrel 文件
  • 你将在编辑器中收到跨包边界导入的自动导入建议。

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

Lint 代码库

要使用 TypeScript 作为 linter,你可以使用 Turborepo 的缓存和并行化来快速检查整个工作区的类型。

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

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

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

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

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

最佳实践

使用 tsc 编译你的包

对于内部包,我们建议你尽可能使用 tsc 来编译你的 TypeScript 库。 虽然你可以使用 bundler,但这没有必要,并且会为你的构建过程增加额外的复杂性。 此外,捆绑库可能会在代码到达你的应用程序的 bundler 之前对其进行修改,从而导致难以调试的问题。

在包边界之间启用跳转到定义

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

即时包

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

编译后的包

来自编译后的包的导出需要使用declarationdeclarationMap 配置才能使跳转到定义工作。 在你为包启用这两个配置后,使用 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 版本。 相反,它将发现一个特定版本并在所有地方使用它。

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