LEARN · DEBUGGING GUIDE

Fixing TypeScript Project References Build Errors

TypeScript project references can fail silently or with cryptic errors. This guide walks through the real causes and fixes for build errors in referenced projects.

AdvancedTypeScript6 min read

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.

( 01 )Fast diagnosis

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
( 02 )Where to look

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
( 03 )Common root causes

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
( 04 )Fix patterns

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
( 05 )How to verify

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
( 06 )Mistakes to avoid

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
( 07 )War story

The Case of the Missing Declaration Files

Senior EngineerTypeScript 5.3, pnpm monorepo, NestJS + shared library

Timeline

  1. 09:15Merge PR adding project references to monorepo. CI fails with TS6059 error: 'File is not under 'rootDir'.
  2. 09:20Check root tsconfig: references point to './lib' and './app'. lib/tsconfig has composite: true but missing declaration: true.
  3. 09:25Add declaration: true to lib/tsconfig. CI still fails – now TS6305: 'Output file ... has not been built from source.'
  4. 09:35Run `npx tsc --build --verbose` locally. See lib compiles but app doesn't pick up lib's .d.ts files.
  5. 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.
  6. 09:55Realize package.json in lib has 'main': './dist/index.js' but 'types' is missing. Add 'types': './dist/index.d.ts'.
  7. 10:00Delete node_modules/.cache/tsbuildinfo. Rebuild with `tsc --build --force`. All green.
  8. 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.

( 08 )Understanding `composite` and `declaration` Requirements

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.

( 09 )Resolving `outDir` and `rootDir` Mismatches

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.

( 10 )Handling Circular Dependencies in Project References

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.

( 11 )Cleaning Stale Build Cache

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.

( 12 )Integrating Project References with Package Managers

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.