TypeScript

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

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

共享 tsconfig.json

你想在 TypeScript 配置中建立一致性,以便你的整个代码库都能使用出色的默认设置,并且你的同事在编写工作区代码时知道该期待什么。

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

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

终端
pnpm dlx 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": "workspace:*",
     "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 字段允许 tsserversrc 中的代码视为代码类型的真相来源。你的编辑器将始终与代码中的最新接口保持同步。
  • 你可以快速地向你的包添加新的入口点,而无需创建 危险的 barrel 文件
  • 你的编辑器将在包边界之间提供导入建议。

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

lint 你的代码库

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

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

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

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

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

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

最佳实践

使用 tsc 编译你的包

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

启用跨包边界的“转到定义”

“转到定义”是一个编辑器功能,可用于通过单击或热键快速导航到符号(如变量或函数)的原始声明或定义。一旦正确配置了 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 版本。相反,它会发现一个特定的版本并在所有地方使用它。

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