A project bundles its TypeScript with esbuild, and the build succeeds and ships even though the source contains type errors. Why does esbuild not fail on them, and what command do teams run to catch them instead?
answer
- two jobs: check and emit
- fast tools do only one of them
- one file at a time, no program
- the checker runs separately, emitting nothing
basics
~20 sesbuild only strips type annotations file by file; it never runs TypeScript's type checker, so type errors cannot fail it. Teams add a separate step running tsc --noEmit, which type-checks the whole program and writes no output.
solid answer
~50 sTypeScript's compiler does two independent jobs: it checks types, and it emits JavaScript with the types erased. Tools like esbuild, swc and `@babel/preset-typescript` implement only the second job — they parse a file, delete the annotations, and emit JavaScript, one file at a time. They never build a whole program, never resolve imported types or `.d.ts` files, and contain no type checker at all, which is exactly why they are so fast. So a type error is invisible to them; only a syntax error can break the build. The correctness half is recovered by running the real compiler in check-only mode, `tsc --noEmit`, as its own script and its own blocking CI job. The practical rule is: the bundler owns emit, `tsc` owns correctness, and if the `tsc` step is missing or non-blocking your types are documentation, not a guarantee.
code
json · 7 lines{
"scripts": {
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js",
"typecheck": "tsc --noEmit",
"ci": "npm run typecheck && npm run build"
}
}go deeper
Know that a bundler-built TypeScript project usually has a separate typecheck script, and that a successful build does not mean the types are correct. Be able to say what tsc --noEmit does.
Explain the split cleanly: transpilers erase types per file with no checker, tsc builds a whole program and checks it. Name esbuild, swc or Babel's TypeScript preset as examples and say why the single-file model is fast.
Show you would wire the gate so it actually gates — a blocking CI job, one shared config, the same command locally and in CI — and name the drift risks when the bundler's emit settings and tsconfig disagree.
Own the tradeoff you are buying: build speed in exchange for a delayed correctness signal that now depends on process rather than the tool. Be ready to argue where the gate belongs in the pipeline and what happens when it is allowed to be non-blocking.
## Two jobs, one traditional tool `tsc` bundles two very different responsibilities. The first is **type checking**: build a program from your `tsconfig.json`, resolve every import, load every `.d.ts`, construct the type graph, and report errors. The second is **emit**: erase the types and write JavaScript at the configured `target` and `module`. The second job is nearly trivial — TypeScript's types are erased, so emitting is mostly deleting annotations and downleveling a few syntax forms. The first job is where essentially all the time goes, because it is global: to check one file the compiler may have to look at hundreds of others. ## Why the fast tools skip checking esbuild, swc and Babel's `@babel/preset-typescript` implement only the emit half. Each of them parses a single file, discards the type syntax, and writes the JavaScript. They do not build a program, do not resolve what an imported symbol's type is, and do not read your declaration files for checking purposes. There is no checker in the binary to run. That single-file model is precisely what buys the speed: files can be transformed in parallel, in any order, with no shared type graph. It is also why the tools are honest about what they promise — esbuild's documentation is explicit that it does not type-check, and Babel's TypeScript preset is a syntax transform. The practical consequence is that the build's success tells you nothing about your types. Code like this transpiles and ships happily: ```ts const id: number = "not a number"; export function greet(u: { name: string }) { return "hi " + u.nmae; // typo, still emits } ``` The emitted JavaScript is just the same code with `: number` and `: { name: string }` removed. Only a **syntax** error — something the parser cannot read — stops a transpile-only build. ## Restoring the gate: tsc --noEmit `--noEmit` runs the full check and writes no output files. That is the whole point: the bundler already produced the output, so you want the compiler purely as a linter of types. ```json { "scripts": { "build": "esbuild src/index.ts --bundle --outfile=dist/index.js", "typecheck": "tsc --noEmit" } } ``` The mirror-image flag also exists: `tsc --noCheck` emits without checking, which is useful when you want TypeScript's own emit but have already checked elsewhere. Where the gate runs matters more than that it exists. Three placements are common, and they are not equivalent: the editor's language service (instant, but a personal setting nobody can enforce), a pre-commit hook (catches things early, bypassable with `--no-verify`), and a **blocking CI job** (the only placement that is actually a guarantee). Most teams keep all three but treat only CI as the gate. ## Failure modes to watch for **Two configurations that disagree.** The transpiler has its own notion of `target`, JSX handling, and decorator support, configured in the bundler — not read from `tsconfig.json` in general. If your `tsconfig.json` says one thing about class field or decorator semantics and the bundler says another, `tsc --noEmit` can be green while the shipped runtime behaviour differs from what the checker modelled. **Different file sets.** If the build compiles `src/` but `tsconfig.json`'s `include` also covers tests and scripts, the two steps are checking different programs. That is usually fine but explains "it builds but typecheck fails" confusion. **A gate that is not a gate.** A `typecheck` script that exists in `package.json` but is not wired into CI, or a CI job marked `continue-on-error`, decays within weeks: errors accumulate and nobody can tell which are new. **Declaration output.** Transpilers emit JavaScript only; producing `.d.ts` files for a published package is still the compiler's job, which is a separate concern from the check gate. ## What you are trading You trade *when* you learn about a type error for *how fast* the build is. In the `tsc`-only world, an error stopped the build immediately. In the split world, a broken build and a broken type are two different signals, arriving at different times, and the discipline of the team — not the tool — is what keeps the second one meaningful.
- If the bundler never reads tsconfig.json for checking, does tsconfig.json still matter in this setup?Yes — it is the input to the gate. `tsc --noEmit`, your editor's language service, and most IDE tooling all read it, so it defines what "type-correct" means for the repo. It also still configures `lib` and module resolution for checking. What it stops governing is emit, which the bundler now owns through its own settings.
- What is the mirror image of --noEmit, and when would you use it?`tsc --noCheck` emits JavaScript while skipping type checking. It is useful when you want TypeScript's own emit — for example declaration output or a downlevel target — but have already run the checker in a different job and do not want to pay for checking twice.
- How would you keep a large team from letting the type-check job rot?Make it blocking on the merge path, not advisory, and keep it a single command any developer can run locally with the same result. Fail on the first error rather than collecting a tolerated-error baseline, because a tolerated list quickly becomes a place errors are added to rather than removed from.
The transpiler is a photocopier that removes the margin notes; the type checker is the editor who reads for meaning. A clean copy proves nothing about whether the text is correct.
saying these in an interview costs you the question
- Thinking esbuild reports type errors it considers serious
- Believing a green build implies the types check
- Assuming the transpiler reads tsconfig.json and enforces it
- Calling tsc --noEmit a way to suppress errors
- Treating the typecheck script as optional if the IDE is green