TypeScript

TypeScript 是 monorepo 中一个出色的工具,允许团队安全地向其 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 字段允许 tsserver 使用 src 中的代码作为代码类型的真实来源。您的编辑器将始终与代码中的最新接口保持同步。
  • 您可以快速向包添加新的入口点,而无需创建 危险的桶文件
  • 您将在编辑器中收到跨包边界的导入的自动导入建议。

如果您要发布包,则不能在 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 库。虽然您可以使用 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 脚本来检查类型时显示的错误之间存在差异。如果这对您来说是一个问题,请考虑保持 TypeScript 依赖项的版本相同