Learn why TypeScript throws TS6305 when a referenced project's declaration output is missing, and how tsc -b builds the graph in the right order.
TS6305 means you asked TypeScript to check a project that depends on another project through references, and the file it wanted to read — the upstream project's emitted .d.ts — is not on disk. The compiler is not confused about your types. It never got as far as your types.
This is the central trade of project references. When project A lists project B in its references array, an import in A that resolves into B's source tree is silently redirected to B's build output: packages/core/src/index.ts becomes packages/core/dist/index.d.ts. That redirect is what makes large monorepos fast, because A never has to re-check B's implementation. It also means B's output is a hard prerequisite. Plain tsc -p does not build prerequisites and does not even check whether they are up to date — it just looks for the file, and reports TS6305 at the import when it is missing.
error TS6305: Output file '/repo/packages/core/dist/index.d.ts'
has not been built from source file '/repo/packages/core/src/index.ts'.
~~~~~~~~~~~~~~~~~
expected here built from thisRead the message as a pair of paths. The second is a source file you own; the first is where the compiler expected that file's declaration output to land, derived from the referenced project's outDir and rootDir. Almost every instance of TS6305 is one of two facts: nothing built that file yet, or it built somewhere other than the path in the message.
One thing TS6305 is not: a staleness check. If the .d.ts exists but is older than the source, tsc -p is perfectly happy and will type-check against the outdated declarations. Noticing that kind of drift is build mode's job, which is one more reason to use it.
The classic: a fresh clone, or a CI job that runs the type-check step before anything has produced dist. Nothing is wrong with either config.
// apps/web/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"],
"references": [{ "path": "../../packages/core" }]
}// ❌ Broken — apps/web/src/main.ts, checked with: tsc -p apps/web/tsconfig.json
import { formatOrderId } from "../../../packages/core/src/index"
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Error: Output file '/repo/packages/core/dist/index.d.ts' has not been built
// from source file '/repo/packages/core/src/index.ts'.
export const orderLabel = formatOrderId({ id: 42, total: 1999 })# ✅ Fixed — build mode walks the reference graph and builds core first
tsc -b apps/webThe source file is untouched. tsc -b notices that packages/core is out of date, compiles it into packages/core/dist, and only then checks apps/web — against the declarations it just produced.
--noEmit feels like it should mean "just look at the types, no build needed". It does not help here, because the file the compiler is missing is an input to your project, not an output of it.
// ❌ Broken — package.json
{
"scripts": {
"typecheck": "tsc --noEmit -p apps/web/tsconfig.json"
}
}Error: Output file '/repo/packages/core/dist/index.d.ts' has not been built
from source file '/repo/packages/core/src/index.ts'.// ✅ Fixed — let build mode produce the declarations the check needs
{
"scripts": {
"typecheck": "tsc -b apps/web"
}
}If you genuinely want no JavaScript on disk, build the upstream project with tsc -b --emitDeclarationOnly instead of dropping the build step. Note that noEmit on the referenced project is not an option either — that combination is rejected with TS6310.
Everything worked, then rm -rf packages/*/dist, a git clean -fdx, a cache-less CI runner or a "clean" npm script removed the declarations, and the next check fails even though nothing changed in the code.
# ❌ Broken — the declarations apps/web reads are gone
rm -rf packages/core/dist
tsc -p apps/web/tsconfig.json
# Error: Output file '/repo/packages/core/dist/index.d.ts' has not been built
# from source file '/repo/packages/core/src/index.ts'.# ✅ Fixed — clean through build mode so the build state goes with the output
tsc -b --clean
tsc -b apps/webDeleting dist by hand leaves tsconfig.tsbuildinfo behind, which still claims everything is up to date. That is why a plain tsc -b after a manual clean can decline to rebuild and leave you staring at the same error; tsc -b --clean removes both, and tsc -b --force ignores the build state altogether.
outDir No Longer Matches Its OutputThe message names a path nobody recognises — lib/index.d.ts when the package clearly builds into dist. Someone changed outDir (or added a declarationDir) in the referenced project without rebuilding it, so the expectation moved and the files did not.
// ❌ Broken — packages/core/tsconfig.json, changed after the last build
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "lib", // build output is still sitting in dist/
"rootDir": "src",
"strict": true
},
"include": ["src"]
}Error: Output file '/repo/packages/core/lib/index.d.ts' has not been built
from source file '/repo/packages/core/src/index.ts'.// ✅ Fixed — one directory, agreed on by tsconfig, package.json and the build
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"]
}Whichever directory you pick, make sure the package's main, types and exports fields point into the same one, and rebuild with tsc -b --force after the change. A mismatch that survives the rebuild usually shows up next as TS2307 — the module cannot be found at all.
Run tsc -b instead of tsc -p. Build mode is the whole point of project references: it reads the graph, builds each out-of-date project in dependency order and then checks yours. Point it at the downstream project (tsc -b apps/web) or at a solution-style root tsconfig.json that references every package. This single change resolves the large majority of TS6305 reports.
Read the first path in the message and check whether that file exists. If it does not, something never built it — go to step 1. If the directory in the path looks unfamiliar, you have an outDir or declarationDir mismatch in the referenced project, and the fix is to align the config with where the build actually writes.
Make the build order explicit in CI. A type-check job that runs before the build will fail on a cold runner every single time. Give your task runner the dependency — "dependsOn": ["^build"] in Turborepo, dependsOn: ["^build"] in an Nx target — or simply run tsc -b at the repo root as the type-check step. Do not paper over it by caching dist in the CI image.
After a manual clean, clean through the compiler. tsc -b --clean deletes outputs and the .tsbuildinfo that tracks them; tsc -b --force rebuilds regardless of what that file claims. Deleting dist with rm -rf and nothing else is what makes this error look sticky.
Do not remove the references entry to make it go away. Dropping the reference so the import resolves to the upstream source is the tempting shortcut, and it trades a clear build-order error for slow rebuilds, rootDir violations (TS6059) and file-list errors (TS6307). The reference is the thing that makes the monorepo incremental — keep it, and keep composite: true on every project that appears in one, so tsc -b can do its job.
A project reference redirects imports from the referenced project's sources to its emitted declarations, and those declarations are missing. TypeScript computes where the .d.ts for a given source file should live from the referenced project's rootDir and outDir, looks there, finds nothing, and reports TS6305 at the import that needed it. Because plain tsc -p never builds anything but the project you named, this happens on every fresh checkout until someone runs a build. It also shows up when a downstream project sets noEmit and has no output of its own — the upstream declarations are still required.
tsc -p tsconfig.json checks and emits exactly one project, and assumes everything it references is already built and current. tsc -b is build mode: it loads the reference graph, decides which projects are out of date by comparing timestamps against each project's tsconfig.tsbuildinfo, builds those in dependency order, and skips the ones that are current.
tsc -b apps/web # builds packages/core first, then apps/web
tsc -b --clean # deletes outputs and build state
tsc -b --force # rebuilds everything, ignoring build stateBuild mode is also the only one of the two that will tell you a referenced project is stale rather than silently type-checking against yesterday's declarations.
Yes — and --noEmit does not change that. The referenced project's .d.ts is an input to your program; your own emit settings say nothing about it. The clean answer is tsc -b, which produces exactly the declarations your check needs and nothing else if the upstream project sets emitDeclarationOnly. One wrinkle worth knowing: since TypeScript 3.7 the editor language service can load a referenced project's sources instead of following the redirect, so the same repository can look perfectly fine in VS Code and fail in CI. When that happens, trust the command line — run tsc -b locally and you will reproduce it.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges