What this usually means
Project references in TypeScript require strict configuration: each referenced project must have `composite: true`, `declaration: true`, and a valid `outDir`. The root project uses `references` to point to these projects, and `tsc --build` compiles them in topological order. Common failures stem from mismatched `outDir` paths, missing or incorrect `declaration` files, circular dependencies, or using `noEmit` in a project that should emit declarations. The build error often masks a deeper configuration inconsistency that the compiler catches only during the build phase.
The first ten minutes — establish facts before touching code.
- 1Run `npx tsc --build --verbose` to see which projects are compiled and what files are emitted
- 2Check each referenced project's tsconfig for `composite: true` and `declaration: true`
- 3Verify `outDir` paths are consistent and that referenced projects actually produce `.d.ts` files
- 4Look for circular references in your project graph by running `npx tsc --build --dry` and inspecting the order
- 5Ensure the root tsconfig uses `references` with correct relative paths (e.g., `{ "path": "./lib" }`)
- 6Check for any `noEmit` or `emitDeclarationOnly` that might conflict with composite mode
The specific files, logs, configs, and dashboards that usually own this bug.
- searchRoot `tsconfig.json`: `references` array, `files`, `include`
- searchEach referenced project's `tsconfig.json`: `composite`, `declaration`, `outDir`, `rootDir`
- search`node_modules/.cache/tsbuildinfo` – corrupted build info can cause stale errors
- search`tsconfig.build.json` if using separate build config
- searchGenerated `.d.ts` files in `outDir` – check they exist and have correct content
- search`package.json` `main`/`types` fields – they might point to wrong output paths
Practical causes, not theory. These are the things you will actually find.
- warningReferenced project has `composite: true` but missing `declaration: true`
- warning`outDir` in referenced project doesn't match what the parent expects (e.g., relative path mismatch)
- warningCircular dependency between projects (A references B, B references A)
- warningUsing `noEmit` in a project that is referenced – composite requires emit
- warningStale `.tsbuildinfo` cache from a previous build with different configuration
- warningInconsistent `rootDir` settings causing incorrect module resolution
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure every referenced project has `composite: true`, `declaration: true`, and `outDir` set
- buildRemove circular dependencies by extracting shared code into a third project or using interfaces
- buildDelete `node_modules/.cache/tsbuildinfo` and rebuild from clean
- buildUse `tsc --build --force` to force rebuild of all projects
- buildSet `rootDir` explicitly in each project to match the source root
- buildFor non-emit projects, use `noEmit: true` only if they are not referenced by others
A fix you cannot prove is a guess. Close the loop.
- verifiedRun `npx tsc --build --clean && npx tsc --build` and check for zero errors
- verifiedInspect generated `.d.ts` files in each project's `outDir`
- verifiedVerify that `import` statements resolve correctly in the IDE after build
- verifiedCheck build timing – incremental builds should be fast after initial compile
- verifiedRun `npx tsc --build --dry` to see the topological order and confirm no cycles
Things that make this bug worse or harder to find.
- warningSetting `outDir` to the same folder for multiple projects without proper `rootDir`
- warningUsing `paths` in tsconfig with project references – they often conflict
- warningIgnoring TS6305 error ('Output file has not been built...') – it signals a real issue
- warningManually editing `.tsbuildinfo` files – always delete and rebuild
- warningAssuming `tsc` without `--build` will use references – it won't
- warningMixing project references with TypeScript's `--watch` mode without proper configuration
The Case of the Missing Declaration Files
Timeline
- 09:15Merge PR adding project references to monorepo. CI fails with TS6059 error: 'File is not under 'rootDir'.
- 09:20Check root tsconfig: references point to './lib' and './app'. lib/tsconfig has composite: true but missing declaration: true.
- 09:25Add declaration: true to lib/tsconfig. CI still fails – now TS6305: 'Output file ... has not been built from source.'
- 09:35Run `npx tsc --build --verbose` locally. See lib compiles but app doesn't pick up lib's .d.ts files.
- 09:45Check lib/outDir: './dist'. App expects './dist/lib'. Root tsconfig references path './lib' but app imports from '@myorg/lib' which maps to node_modules.
- 09:55Realize package.json in lib has 'main': './dist/index.js' but 'types' is missing. Add 'types': './dist/index.d.ts'.
- 10:00Delete node_modules/.cache/tsbuildinfo. Rebuild with `tsc --build --force`. All green.
- 10:05CI passes. Root cause: missing declaration and types field, plus stale cache.
The morning started with a CI failure that seemed like a simple config issue. The team had migrated to a monorepo with project references for better incremental builds. The error was TS6059: 'File is not under 'rootDir''. I'd seen this before – it usually means rootDir is misconfigured. But digging deeper, I found the lib project had `composite: true` but no `declaration: true`. TypeScript requires declarations for referenced projects so consumers can type-check without recompiling.
After fixing that, a new error appeared: TS6305 – 'Output file has not been built from source.' This one is tricky. It means the build info cache thinks a file is up-to-date but its source has changed. I suspected a stale cache. But I also noticed the app couldn't resolve the lib's types. The lib's package.json was missing the `types` field. TypeScript's project references use `.d.ts` files, but if the package doesn't point to them, Node resolution fails.
I deleted the build cache and added the `types` field. Then I ran `tsc --build --force` to rebuild everything from scratch. It worked. The lesson: project references require meticulous configuration across tsconfig, package.json, and the build pipeline. One missing field can cascade into cryptic errors. Now we have a CI step that validates `types` fields in referenced packages.
Root cause
Missing `declaration: true` in referenced project's tsconfig and missing `types` field in its package.json, compounded by stale build cache.
The fix
Added `declaration: true` to lib/tsconfig, added `"types": "./dist/index.d.ts"` to lib/package.json, and cleared the build cache with `rm -rf node_modules/.cache/tsbuildinfo`.
The lesson
For project references, always verify three things: tsconfig composite/declaration/outDir, package.json types/main, and clear cache on config changes.
When a project is referenced, TypeScript needs to emit declaration files (`.d.ts`) so the parent project can type-check without compiling the dependency. This is enforced by the `composite` option, which implicitly sets `declaration: true` and `declarationMap: true`. However, if you set `composite: true` but override `declaration: false`, you'll get an error. Always let `composite` manage these options.
Additionally, `declarationMap` helps with go-to-definition across project boundaries. Without it, IDE navigation may break. Make sure each referenced project has `composite: true` and does not set `noEmit` or `emitDeclarationOnly` unless you know exactly what you're doing.
A common source of TS6059 is when `rootDir` is set to a parent directory of the source files, but `outDir` doesn't match the expected output structure. For example, if a project in `./lib` has `rootDir: "src"` and `outDir: "dist"`, the emitted files will be in `./lib/dist`. The parent project's `references` path must be `./lib`, and its imports should resolve to the correct subpath.
A reliable pattern: set `rootDir` to the project's source root (e.g., `src`) and `outDir` to a sibling directory like `dist`. Ensure the parent project does not have its own `rootDir` that conflicts. Use `tsc --build --verbose` to see exactly where each file is emitted.
TypeScript does not allow circular project references. If project A references B and B references A, compilation fails with a cycle error. The solution is to extract the shared types or interfaces into a third project that both A and B reference. Alternatively, if the cycle is caused by a single type, inline it or use a forward reference.
Detecting cycles: run `tsc --build --dry` to see the order of compilation. If you see a project listed before its dependency, you have a cycle. Use tools like `madge` to visualize the dependency graph.
The `.tsbuildinfo` files cache module resolution and declaration outputs. When you change project references or tsconfig options, the cache can become stale, leading to TS6305 errors. The fix is to delete all `.tsbuildinfo` files and rebuild. A quick command: `find . -name '*.tsbuildinfo' -delete && tsc --build`.
I've also seen cases where the cache gets corrupted due to concurrent builds. Use `tsc --build --force` to ignore the cache entirely. In CI, it's safer to always do a clean build by deleting the cache directory first.
In a monorepo with pnpm or yarn workspaces, project references can conflict with node module resolution. The `references` in tsconfig tell TypeScript where to find the `.d.ts` files, but runtime imports go through `node_modules`. Your `package.json` must have correct `main` and `types` fields pointing to the emitted files.
I recommend using TypeScript's `project references` for development and type-checking, but during build, use a bundler (esbuild, webpack) that respects the `types` field. Don't rely on `tsc` to bundle for production. Also, avoid using `paths` in tsconfig as it can confuse resolution with references.
Frequently asked questions
Why do I get 'Project references may not be used with noEmit'?
If a project is referenced by others, it must emit declarations. Setting `noEmit: true` prevents that. Either remove `noEmit` from the referenced project or make it a non-referenced project. Use `composite: true` instead, which ensures proper emit.
How do I fix 'File is not under rootDir' error with project references?
This error means a source file is outside the project's `rootDir`. Ensure each referenced project has its own `rootDir` set to the directory containing its source files (e.g., `src`). Also, verify that the parent project does not include files from referenced projects directly in its `include` or `files` arrays.
What is the difference between `tsc --build` and `tsc -p tsconfig.json`?
`tsc -p tsconfig.json` compiles a single project. `tsc --build` (or `tsc -b`) compiles the project and all its references in topological order, using build info for incremental compilation. Always use `--build` when using project references.
Can I use project references with `--watch` mode?
Yes, but it has limitations. Use `tsc --build --watch`. It will watch all referenced projects for changes. However, if you change a referenced project's tsconfig, you may need to restart the watch. Also, ensure no two projects write to the same `outDir` or the watch may interfere.
Why does the IDE show errors but `tsc --build` succeeds?
This often happens when the IDE's TypeScript version differs from the project's, or the IDE uses a different tsconfig (e.g., `tsconfig.json` vs `tsconfig.build.json`). Ensure the IDE is using the same config and that it reloads after changes. Also, check for `exclude` patterns that might hide files from the IDE.