What does running `tsc --build` (`tsc -b`) do that plain `tsc -p tsconfig.json` does not?
answer
- one project versus the whole graph
- topological order, then skip the unchanged
- timestamps plus recorded build state
- identical declarations stop the cascade
- --verbose explains every decision
basics
~20 sBuild mode walks the references graph: it builds each referenced project first, in dependency order, and skips any project whose outputs are already newer than its inputs. Plain tsc -p compiles only the one project and assumes its dependencies are already built.
solid answer
~50 s`tsc -p` compiles exactly one project. If that project references others, it expects their `.d.ts` output to exist already and errors with "Output file … has not been built from source file …" when it does not. `tsc --build` is a different mode: it reads the `references` array transitively, topologically sorts the graph, and builds each project in order, consulting each project's `.tsbuildinfo` and output timestamps to skip anything already up to date. It also understands the graph well enough to stop early — if rebuilding an upstream project produces byte-identical declarations, downstream projects need no rebuild. Build mode has its own flags: `--clean` deletes outputs, `--force` rebuilds everything regardless of up-to-date state, `--dry` reports what it would do, `--watch` keeps the whole graph live, and `--verbose` prints why each project was or was not rebuilt.
code
bash · 9 lines# Build the whole reference graph, explaining each decision
npx tsc -b packages/app --verbose
# Show the plan without touching the filesystem
npx tsc -b packages/app --dry
# Reset: delete every project's outputs, then rebuild from scratch
npx tsc -b packages/app --clean
npx tsc -b packages/app --forcego deeper
Know that tsc -b builds a project together with everything it references, in the right order, while plain tsc builds only the one project. Recognise the not-built-from-source error as a missing dependency build.
Explain the up-to-date check — build info plus input/output timestamps — and the main build-mode flags (--clean, --force, --dry, --watch, --verbose), plus why a reference resolves to emitted declarations rather than sources.
Show you can reason about rebuild cascades in a real repo: why a signature change fans out while a body change does not, and how you use --verbose output to explain a slow pipeline to a team.
Own how tsc -b fits into the wider pipeline — what it should orchestrate versus what a task runner owns, and how the shape of the reference graph determines how much of the repo a typical change rebuilds.
## Two different programs behind one binary `tsc` has two operating modes. The ordinary mode takes a single project — the nearest tsconfig.json, or the one you pass with `-p`/`--project` — reads its file list, type-checks it, and emits. It knows nothing about building anything else. Build mode, entered with `-b` or `--build`, is an orchestrator. It takes one or more projects, follows their `references` arrays transitively to discover the whole dependency graph, orders it topologically, and then runs the ordinary compilation for each project that needs it. ```bash tsc -b packages/app # build app and everything it references tsc -b --verbose # explain each decision tsc -b --clean # delete outputs of the whole graph tsc -b --force # rebuild everything, ignore up-to-date checks tsc -b --watch # keep the graph rebuilding on change ``` ## What happens without build mode Run `tsc -p packages/app/tsconfig.json` in a fresh checkout where `packages/core` has never been compiled and you get an error like: ``` error TS6305: Output file '/repo/packages/core/dist/index.d.ts' has not been built from source file '/repo/packages/core/src/index.ts'. ``` This is the single most instructive error in the whole feature, because it tells you exactly how references work. Project `app` does not type-check `core`'s sources — it resolves imports of `core` to `core`'s **emitted declarations**. If those declarations are missing, the reference cannot be satisfied, and single-project mode has no idea how to produce them. Build mode does: it sees the reference, builds `core` first, and only then compiles `app`. ## The up-to-date check The value of build mode in a repo of any size is not ordering — you could script that — it is skipping. For each project in the graph, build mode decides between three broad outcomes: build it, skip it as up to date, or skip it because only its upstream types changed in ways that do not affect it. The decision draws on two things. The project's `.tsbuildinfo` file records what was compiled and with which compiler version and options. Timestamps compare the project's inputs against its emitted outputs. If an output is older than an input, the project is out of date and gets rebuilt; if the config changed or the compiler version differs from the one recorded, the build info no longer applies and the project is rebuilt too. There is a second-order optimisation worth knowing: after rebuilding an upstream project, build mode compares the freshly emitted declarations with the previous ones. If a change was purely in implementation — a function body edited, no signature moved — the `.d.ts` output is unchanged and downstream projects are left alone. This is why a one-line change deep in a shared package sometimes rebuilds one project and sometimes rebuilds twenty: the difference is whether the public type surface moved. `--verbose` makes all of this legible, printing lines such as "Project 'packages/app' is out of date because output 'dist/index.js' is older than input 'src/index.ts'" — the fastest way to answer "why is this rebuilding again?". ## The build-mode flags `--clean` removes the outputs of every project in the graph, including build-info files; it is the reset button when you suspect the incremental state rather than the code. `--force` builds everything without consulting up-to-date state, which is the safer thing to reach for in a pipeline you do not trust. `--dry` prints the plan without doing anything. `--watch` runs the whole graph in watch mode, rebuilding the affected subgraph on each change rather than restarting from scratch. A practical constraint: in build mode most compiler options cannot be passed on the command line, because each project's settings come from its own tsconfig.json. Build mode accepts its own flags plus a small set of others; anything that changes checking or emit belongs in the config file. ## Where it does and does not help Build mode's job ends at ordering and skipping TypeScript compilations. It does not bundle, does not run tests, and does not know about anything in the repository that is not a referenced TypeScript project. Repos that also need to sequence non-TypeScript steps typically drive `tsc -b` from a task runner rather than expecting it to be one. The payoff is real but bounded: the first build of a graph costs slightly more than a single monolithic compilation, because declarations must be written and read at each boundary. Everything after that is where you win — an edit confined to one leaf package rebuilds one project instead of the world.
- Why does a plain `tsc -p` on a referencing project fail in a fresh checkout with "Output file has not been built from source file"?Because a referencing project resolves imports of a referenced project to that project's emitted `.d.ts` files, not to its sources. In a fresh checkout those declarations do not exist yet, and single-project mode has no mechanism to build another project. `tsc -b` resolves it by building the dependency first.
- You edit a function body in a shared package and only that package rebuilds. Change its return type and twenty projects rebuild. What explains the difference?Build mode compares the newly emitted declarations against the previous ones. A body-only edit leaves the `.d.ts` output identical, so downstream projects stay up to date. Changing a signature changes the declarations, which invalidates every project that consumes them and cascades through the graph.
- When would you reach for `--force` rather than letting the up-to-date check do its job?When you do not trust the incremental state: a release build, a pipeline that restored a partial cache, or after a compiler upgrade or config change you are not certain invalidated everything. `--force` trades time for certainty. `--clean` goes further by deleting outputs first, which is what you want when stale files may be lingering.
saying these in an interview costs you the question
- Thinks tsc -b just runs tsc in every folder
- Believes referencing projects type-check their dependency's sources
- Says build mode also bundles or runs tests
- Assumes any compiler flag can be passed alongside -b
- Confuses --clean with --force