Learn why TypeScript throws TS5055 when an output file would land on one of its own inputs, and how outDir, exclude and declaration settings fix it.
TS5055 means TypeScript worked out where one of its output files should go, discovered that a file already lives at that exact path, and noticed that this file is one of its own inputs. Rather than destroying your source, the compiler stops and tells you which path is both read and written.
This is an emit-time check, not a type check. Before writing anything, tsc computes the output path of every file in the program — the .js, the .d.ts, the .js.map — and compares each one against the set of files it parsed. A match is an error, and the error is reported once per colliding path. Nothing about your types is wrong; the file layout is.
Because it happens at emit, the flags that matter are the ones that decide what is read (include, exclude, files, allowJs) and where things are written (outDir, rootDir, declaration, declarationDir). Strictness settings are irrelevant, and tsc --noEmit never reports TS5055 at all — with nothing being written, there is nothing to collide.
error TS5055: Cannot write file '/app/src/formatMoney.js' because it would overwrite input file.
~~~~~~~~~~~~~~~~~~~~~~~
this path is in the program AND is an emit targetThe single most useful question to ask is: which file is on both sides? The path in the message is the answer — it is simultaneously something the compiler read and something it wants to produce.
allowJs With No outDirTurning on allowJs makes .js files inputs. Without an outDir, the output for formatMoney.js is… formatMoney.js. Every single JavaScript file in the project collides on the very first build.
// ❌ Broken — tsconfig.json
{
"compilerOptions": { "allowJs": true, "strict": true },
"include": ["src"]
}[object Object]// ✅ Fixed — send emit somewhere else, and pin the input root
{
"compilerOptions": {
"allowJs": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"]
}rootDir is not decoration here. It fixes the folder that output paths are computed relative to, so a stray input from outside src fails with the much clearer TS6059 instead of quietly changing where everything lands.
exclude That Loses The outDir ExemptionThis is the one that looks haunted: the first build is clean, the second fails. TypeScript excludes outDir from the default input glob — but only while exclude is unspecified. Write your own exclude, and that automatic exemption disappears.
// ❌ Broken — "exclude" replaces the defaults, so "dist" is source again
{
"compilerOptions": { "allowJs": true, "outDir": "dist", "strict": true },
"include": ["**/*"],
"exclude": ["node_modules"]
}// first build: clean, writes dist/formatMoney.js
// second build:
error TS5055: Cannot write file '/app/dist/formatMoney.js' because it would overwrite input file.// ✅ Fixed — put the output folder back in exclude and narrow include
{
"compilerOptions": {
"allowJs": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}If you take one habit from this page: whenever you add an exclude array, re-add your build folder to it by hand.
include Picks Back UpThe same trap, wearing a .d.ts costume. Here declarationDir writes into types/, and types is also listed in include, so last build's declarations come back as inputs.
// ❌ Broken — types/ is both the declaration target and an input folder
{
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "types",
"strict": true
},
"include": ["src", "types"],
"exclude": ["node_modules"]
}[object Object]// ✅ Fixed — compile src, consume types, never both
{
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"declarationDir": "types",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "types"]
}An output directory should never appear in include. Consumers read it through your package's types entry, not through your own compilation.
files List That Names A .d.ts Next To Its .tsWhen include is a glob and a folder holds both settings.ts and settings.d.ts, TypeScript quietly drops the declaration file and compiles the .ts — no error. Name both explicitly in files, though, and the hand-written declaration becomes a real input sitting on the declaration emit target.
// ❌ Broken — settings.d.ts is an input, and also where settings.ts emits
{
"compilerOptions": { "declaration": true, "strict": true },
"files": ["src/settings.ts", "src/settings.d.ts"]
}[object Object]// ✅ Fixed — give the ambient declarations a name no source file can claim
{
"compilerOptions": { "declaration": true, "outDir": "dist", "strict": true },
"files": ["src/settings.ts", "src/global.d.ts"]
}Ambient files such as global.d.ts, env.d.ts or images.d.ts are fine as long as no .ts file shares their base name in the same folder. The moment someone adds env.ts beside env.d.ts, the pair is a loaded gun.
Read the path in the message, then find it twice. The message names one file. Work out how it got into the program (a glob in include, an entry in files, a relative import) and which source emits onto it. Everything else follows from that one answer.
Give the compiler somewhere else to write. Set outDir (and declarationDir if you split declarations out), and keep the output folder out of include. If the only thing you want from a JavaScript codebase is types, emitDeclarationOnly sidesteps the problem entirely — no .js is written, so no .js can be overwritten.
Exclude your build folder explicitly. "exclude": ["node_modules", "dist"] — always both. The automatic outDir exemption only applies while exclude is absent, so the day someone adds a single entry to that array they also, invisibly, start compiling dist.
Set rootDir and let it catch mistakes early. With rootDir: "src", a file that sneaks in from outside — another package's sources reached through a relative ../../shared/src/money import, say — fails with TS6059 naming that file, instead of silently shifting every output path. In a monorepo, import across packages by package name through paths, or wire the packages together with project references and composite, so a sibling's sources never join this program in the first place.
Do not "fix" it by deleting the output folder. rm -rf dist makes the next build pass and the one after fail again, and in watch mode or under ts-loader the error comes back the moment an output file is re-created. Treat a green build after a clean as confirmation of the diagnosis, not as the fix — the config change is the fix. Equally, do not reach for noEmit to quiet it: that turns off the very output you were trying to produce.
Before emitting, the compiler maps every file in the program to its output paths and checks them against the program's own input list. When an output path is also an input path, you get TS5055 rather than a destroyed source file. Three setups produce almost all real cases: allowJs without an outDir, so each .js compiles onto itself; an output folder that include or a hand-written exclude allowed back in as source; and a .d.ts that occupies exactly the path the declaration emit wants. The check runs on file paths, not on types, so nothing in your code needs to change — only tsconfig.json.
Because the first build creates the file that the second one collides with. A fresh clone has no dist, so the input set is just your sources and everything emits fine. Now dist exists, and if your config lets those files be treated as inputs the next run parses them and computes outputs that land straight back on top. The tell is that rm -rf dist && tsc succeeds and an immediate second tsc fails. It also explains why the error shows up in CI but not locally, or the other way round, depending on who has a stale build folder lying around.
Set outDir and either leave exclude unspecified or list the output folder in it — TypeScript only excludes outDir automatically while you have not written your own exclude array. The robust shape is "include": ["src"] plus "exclude": ["node_modules", "dist"] plus "rootDir": "src": include limits the glob to real sources, exclude covers the case where output lands somewhere unexpected, and rootDir turns any remaining stray input into an explicit TS6059 that names the offender. Adding dist to .gitignore does nothing for this — tsc reads the filesystem, not git.
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