skip to content

You lead a team whose build strips TypeScript types without checking them. How would you decide where the type-check gate runs — the editor, a pre-commit hook, or CI — and what does each placement cost?

level: principalimportance: nice to knowfreq 28%

answer

  1. separate feedback from enforcement
  2. only one placement can actually block
  3. same command, same config, everywhere
  4. partial checking is unsound, not just weaker
  5. a non-blocking gate rots quickly

basics

~20 s

Treat the editor as feedback, a pre-commit hook as convenience, and a blocking CI job as the only real enforcement. Put the guarantee in CI, keep the other two as fast loops, and run the identical tsc --noEmit command everywhere.

solid answer

~50 s

Separate feedback from enforcement. The editor gives the fastest signal but is a per-developer setting nobody can enforce, and it silently checks only what is open. A pre-commit hook catches errors before they leave the machine, but it is bypassable with `--no-verify`, adds seconds-to-minutes to every commit, and is dangerous if it checks only staged files — type errors are cross-file, so a per-file check is unsound. The blocking CI job running `tsc --noEmit` over the whole program is the only placement that is actually a guarantee, so that is where the gate belongs. I keep the other two as accelerators, insist all three run the same command against the same config so results cannot diverge, and if CI latency is the complaint I fix it with parallelism and real diagnostics rather than by narrowing what the gate covers.

go deeper

for a junior

Understand that a type-check step must actually block a merge to mean anything, and that seeing errors in your editor is not the same as the project enforcing them.

for a middle

Be able to compare the three placements on feedback speed versus enforceability, and explain why checking only changed or staged files gives unsound results.

for a senior

Show you would standardise one command and one config across local and CI, keep the CI job blocking, and address latency by parallelising rather than shrinking the checked program.

for a principal

Own the whole policy and its decay: which guarantee lives where, what you would give up first, why baselines and advisory jobs erode the gate, and how you make weakening it a visible decision rather than a quiet one-line change.

## The decision underneath the question Once the build no longer type-checks, type safety stops being a property of the tooling and becomes a property of the process. So the question is not "where can we run `tsc --noEmit`" — it is "where does a failure actually stop something from shipping". Everything else is feedback, which is valuable but is not a gate. ## The three placements, honestly assessed **Editor / language service.** Latency near zero, and it is where developers actually notice mistakes. But it is configured per machine, it can be disabled or ignored, and it checks the dependency graph reachable from what you have open rather than the whole program — so a change that breaks a distant consumer can look clean. Excellent feedback, worthless as enforcement. **Pre-commit hook.** Catches problems before they reach a shared branch, and gives fast, local failure. The costs are real: every commit pays the check's latency, which on a large program means people start committing less often or reach for `--no-verify`. Worse is the tempting optimisation of checking only the staged files — type errors are inherently cross-file, so a per-file check both misses errors and reports phantom ones. A whole-program check in a hook is sound but slow; a partial one is fast but wrong. **CI job.** Slowest feedback, and the only placement that is enforceable. It runs the same command on a known machine over the entire program, and if it is *blocking* on the merge path, no unchecked code lands. This is where the guarantee goes. ## The rules I would set 1. **One command, one config.** `tsc --noEmit` against the repo's `tsconfig.json`, invoked identically in the hook, in CI, and by a developer typing `npm run typecheck`. The instant CI runs a different command or a different config from what a developer can run, "green locally, red in CI" becomes normal and people stop trusting the gate. 2. **The CI job is blocking, always.** A non-blocking check is a log message. Errors accumulate, nobody can tell new from old, and within a quarter the number is too large to fix. 3. **No error baselines.** A tolerated-error list becomes a place to add errors rather than remove them. If a migration genuinely needs a staged rollout, scope it by directory with an explicit, dated plan to close it — not by an open-ended allowance. 4. **Fix latency with parallelism, not with coverage.** Run the type-check job concurrently with build and tests rather than shrinking the program it checks. If it is genuinely slow, diagnose it with the compiler's own instrumentation instead of excluding directories, since the directories people exclude first — tests, scripts — are exactly the ones where types are quietly rotting. 5. **Tests and scripts are in the program.** They are the code most likely to be excluded and the code where an unchecked type error costs you a false-confidence green suite. ## What I would trade away I would drop the pre-commit hook before I dropped anything else. It duplicates a guarantee CI already provides, it taxes the most common developer action, and it teaches people to use `--no-verify` — a habit that later bypasses hooks that do matter. Push-time or pull-request-time checking is the better compromise when the team wants earlier feedback than CI. I would also accept that developers see type errors slightly later than they did in the `tsc`-builds-everything world. That is the deal the fast build bought us, and the way to pay it back is a check that runs continuously alongside the dev server rather than a hook wedged into the commit. ## The failure mode to name out loud The thing that actually destroys these setups is not a wrong placement — it is a gate that exists on paper. A `typecheck` script in `package.json` that no workflow invokes; a CI job set to continue on error "temporarily"; a runner switched to a transpile-only mode for speed when it was the only checker in the pipeline. Each of those is a one-line change that silently deletes the guarantee, and none of them breaks a build, so nothing announces it. Auditing that the gate is real, and staying alert to changes that quietly weaken it, is more of the job than choosing between the three placements.

  • Why is a pre-commit hook that checks only the staged files unsound?
    Because type errors are cross-file by nature. Changing an exported signature breaks consumers in files you did not stage, so a staged-file check misses real errors; it can also report errors that whole-program context would resolve. Soundness requires checking the program, which is exactly what makes the hook slow.
  • A team wants a baseline of tolerated type errors so the gate can be turned on today. How do you respond?
    Prefer a scoped rollout over a tolerated list: enable the gate fully for a directory or package and expand, with dates. A baseline file becomes an append target — the count grows because adding to it is easier than fixing — and it removes the one property that makes a gate useful, which is that red means new.
  • How do you keep the gate honest against changes that quietly remove it?
    Make the weakening visible: the typecheck job is a required check on the merge path, so setting it non-blocking is a settings change someone must approve, and the command lives in one script that shows up in review when edited. Also watch runner flags — switching a script runner to transpile-only can delete the only checker in a pipeline.

saying these in an interview costs you the question

  • Treating editor errors as an enforcement mechanism
  • Letting the CI type-check job be advisory
  • Checking only staged files in a commit hook
  • Excluding tests and scripts to make the gate faster
  • Adopting a growing baseline of tolerated errors

context