Monorepo 中如何正确共享 TypeScript 配置?

书生シ子睿 阅读 13

我在用 pnpm 搭建的 Monorepo 项目里,想让多个子包共享同一套 tsconfig.json,但每次改完 root 的配置,子包里的类型检查就报错,比如找不到模块或者路径别名解析失败。

我试过在子包里用 "extends": "../../tsconfig.base.json",也配了 pathsbaseUrl,但 VS Code 还是提示类型错误,build 时也出问题。是不是还要配什么编译选项或者引用方式不对?

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@utils/*": ["packages/utils/src/*"]
    }
  }
}
我来解答 赞 3 收藏
二维码
手机扫码查看
2 条解答
Des.筱萌
这个问题我之前也踩过坑,TypeScript 官方文档对 Monorepo 的配置其实有专门的说明,主要是 baseUrl 和 paths 的解析机制跟你想象的不太一样。

按照规范,问题出在你子包里直接配了 "baseUrl": ".",这会让 paths 的解析起点变成子包目录,而不是 root 目录。TypeScript 的 paths 是相对于 baseUrl 解析的,所以你配的 @utils/* 实际上是在找 packages/utils/src/packages/utils/src/*,当然找不到。

正确的做法是这样:

根目录的 tsconfig.base.json 放公共配置:

{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"baseUrl": ".",
"paths": {
"@utils/*": ["packages/utils/src/*"]
}
}
}


子包的 tsconfig.json 继承时,不要覆盖 baseUrl:

{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}


如果你的子包需要引用其他包的类型,建议按照 TypeScript 官方推荐的方式配置 Project References。根目录加一个 tsconfig.json

{
"references": [
{ "path": "./packages/utils" },
{ "path": "./packages/app" }
],
"files": []
}


子包里也要加上 composite: true 和对应的 references:

{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../utils" }
],
"include": ["src"]
}


另外 pnpm 的 workspace 依赖要用 workspace:* 协议,确保依赖能正确解析。还有一点,VS Code 有时候缓存不会立刻刷新,改完配置后跑一下 tsc --build 看看命令行是否正常,命令行没问题那就是 VS Code 的缓存问题,重启一下或者执行 TypeScript: Restart TS Server。
点赞
2026-03-02 05:01
淇轩
淇轩 Lv1
根本原因是你只在 root 写了个基础 tsconfig,但没让每个子包真正“继承”并“激活”这套配置,尤其是路径映射(paths)这种东西,它必须配合 tsconfig.jsoncompositereferences 机制,以及每个子包自身的 tsconfig.json 正确引用 root 的配置,才能让 tsc 和 VS Code 都认得路径别名。

我用 pnpm + ts + monorepo 的经验说,正确的做法分三步走:

第一步,root 下的 tsconfig.base.json 要严格限制内容,它只能放通用配置,不能包含 filesincludeexclude,因为这些会覆盖子包的扫描范围;同时 paths 要写绝对路径,不能用相对路径,因为子包运行时的工作目录不同,相对路径会崩。你那个 "@utils/*": ["packages/utils/src/*"] 就是错的——它在 root 看没问题,但在 packages/frontend 里跑 tsc 时,它会把 . 当成当前子包目录,自然就找不到 packages/utils/src/*

所以 root 的 tsconfig.base.json 应该这样写:

{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"baseUrl": ".",
"paths": {
"@utils/*": ["./packages/utils/src/*"]
}
}
}


注意这里 composite: true 必须有,它让这个 tsconfig 能被其他项目引用;baseUrl: "." 是必须的,它代表当前 monorepo 根目录;paths 里的路径一定要加 ./ 前缀,否则 tsc 会把它当成绝对路径,但又不是系统绝对路径,而是相对于 baseUrl 的路径,所以 ./packages/... 才是安全写法。

第二步,每个子包(比如 packages/frontend)都要有自己的 tsconfig.json,并且用 extends 引用 root 的 base 配置,同时补全 includereferences

{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"]
},
"include": ["src/**/*"],
"references": [
{ "path": "../utils" }
]
}


这里有两个关键点:
第一个是 extends 路径要准确,从当前子包目录往回找;
第二个是 references 不能少,它告诉 tsc 这个包依赖了哪个子包(这里是 ../utils),tsc 才会先编译被依赖的包,并正确解析它的类型导出。如果你不写 references,即使 paths 生效了,tsc 也会报错说找不到模块,因为它根本没去读 utils 包的 tsconfig.tsbuildinfo 文件。

第三步,确保每个子包的 package.jsontype 字段和 main/types 字段匹配,比如:

{
"name": "@monorepo/frontend",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}


否则即使 tsc 编译成功,运行时也可能因为模块格式不匹配(比如你用了 ES module 但 main 指向的是 cjs 文件)导致类型解析失败。

另外,VS Code 的 TypeScript 插件默认不会加载 references,你需要在 VS Code 设置里开这个选项:

{
"typescript.tsdk": "node_modules/typescript/lib"
}


或者更稳妥的做法是在项目根目录建一个 .vscode/settings.json

{
"typescript.tsdk": ".pnpm/node_modules/typescript/lib"
}


因为 pnpm 会把 typescript 装在 .pnpm/ 里,VS Code 如果找不到正确的 typescript 版本,就可能用系统自带的旧版,路径解析逻辑不一样,容易出错。

最后说个常见坑:如果你用 ts-nodetsx 这类运行时工具,它们默认不走 tsconfig.jsonpaths,除非配合 tsconfig-pathsmodule-alias。比如:

ts-node -r tsconfig-paths/register src/index.ts


或者在 tsconfig.json 里加 "moduleResolution": "bundler"(TypeScript 5.0+ 支持),这样运行时也能正确解析路径别名,前提是你的构建工具(比如 vite、webpack)也支持这个模式。

总结一下,Monorepo 共享 tsconfig 的核心逻辑是:
root 只定义基础配置 + composite: true + paths 用相对根目录的路径
每个子包用 extends 引用 root + references 声明依赖 + include 控制扫描范围
最后确保运行时工具也能识别这些配置

我之前踩过好多次坑,后来发现只要 referencescomposite 都配齐,90% 的类型错误都能解决,剩下的就是路径写错或者子包没 build 完就引用了。
点赞
2026-02-26 19:01