rootDir & outDir
rootDir and outDir shape the directory layout of a compiled TypeScript project. outDir tells the compiler where to put the emitted .js (and .d.ts, .map) files; rootDir tells it which folder to treat as the root of your source tree so it can mirror that structure in the output. Together they keep generated files out of your source folders and produce a clean, predictable dist/. This page explains how each works, how they interact, and the subtle ways rootDir can be inferred incorrectly.
What outDir does
outDir redirects all emitted files into a single output folder instead of writing .js next to each .ts. This keeps source directories clean and gives you one folder to publish or .gitignore.
{
"compilerOptions": {
"outDir": "./dist"
}
}
With this set, compiling src/index.ts writes dist/index.js rather than src/index.js. Without outDir, the compiler emits beside the source, which clutters your tree and risks committing build artifacts.
What rootDir does
rootDir declares the longest common parent of your input files. The compiler reproduces the folder structure relative to rootDir inside outDir. It also acts as a guard: emitting a file from outside rootDir is an error.
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
}
}
Here src/api/users.ts compiles to dist/api/users.js. The src/ prefix is stripped because it is the rootDir, so the output mirror starts one level deeper.
How the two map together
The output path for any file is computed by removing the rootDir prefix and prepending outDir. The table shows the mapping for a typical layout.
| Source file | rootDir | outDir | Emitted file |
|---|---|---|---|
src/index.ts | ./src | ./dist | dist/index.js |
src/api/users.ts | ./src | ./dist | dist/api/users.js |
src/index.ts | (inferred) | ./dist | dist/index.js |
index.ts + src/a.ts | (inferred .) | ./dist | dist/index.js, dist/src/a.js |
The last row is the classic surprise: add one file at the project root and the inferred rootDir becomes ., so every output gains an extra src/ level.
Why you should set rootDir explicitly
If you omit rootDir, TypeScript infers it as the longest common path of all included files. Add or remove a file and the inferred root can shift, silently changing your entire output layout. Setting it explicitly locks the structure.
// Explicit rootDir keeps output stable no matter which files are added
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"sourceMap": true
}
}
// ❌ Error: File '/project/scripts/seed.ts' is not under 'rootDir' '/project/src'.
// Fix: move the file under src, or widen rootDir, or exclude it.
import "../scripts/seed";
The error above is rootDir doing its job — it flags files that would otherwise escape the mirrored structure. Adjust include/exclude or the rootDir value rather than ignoring it.
Related options
outDir pairs naturally with a few others. declaration and sourceMap emit alongside the .js into outDir. declarationDir can split .d.ts files into their own folder, and rootDirs (plural) merges several virtual roots — useful for generated code — but most projects only need the singular forms.
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationDir": "./types"
}
}
Add your
outDir(e.g.dist/) to.gitignore. Compiled output is a build artifact — committing it leads to stale, conflicting files and noisy diffs.
Best Practices
- Always set
outDirso generated files never mix with source files. - Set
rootDirexplicitly to./src(or your source root) to keep output structure stable. - Treat the “not under rootDir” error as a signal to fix
include/exclude, not to removerootDir. - Add
outDirto.gitignore— never commit compiled artifacts. - Keep all compilable sources under one folder so
rootDirstays simple. - Use
declarationDironly if you need.d.tsfiles separated from JavaScript output. - Clean
outDirbetween builds (or usetsc --build) to avoid leftover stale files.