We put a ceiling on our TypeScript errors. It only moves down.
A second ratchet, on a different metric, proving the pattern generalizes
A few weeks ago I wrote about replacing a hard 80% coverage gate with a ratchet: one committed number, CI fails only if you go backward. The response I got most often was some version of "does that actually generalize, or does it only work for coverage." Fair question. Here is the second one, on a completely different metric, running in the same monorepo.
Our shared Express API touches 15+ route namespaces and gets edited by a dozen different app teams. tsc --noEmit on it does not return zero errors. It never has. Some of those errors are years old, tied to types that predate a library upgrade, wired into code nobody wants to touch for a two-line fix. A hard "zero TypeScript errors" gate would have blocked every PR since the day someone turned it on.
So the gate does not ask for zero. It asks for not-worse. A single constant in the type-check script holds the ceiling, currently 2875. CI runs the full type-check, counts every line containing error TS, and fails only if that count goes above the ceiling. Fix two old errors while you're in the file for something else, and the count drops to 2873. Nothing forces you to lower the ceiling to match, but the script nudges you when you're sitting comfortably below it, the same "you've earned a raise" prompt the coverage ratchet uses.
The measured count right now is 2860, fifteen below the ceiling. That gap did not come from a cleanup sprint. It came from the same effect the coverage ratchet produced: once the only way to fail CI is going backward, the path of least resistance for anyone touching old code is to leave it slightly better than they found it, instead of routing around it.
One deliberate design choice worth calling out: the gate treats a crash as a failure, not a pass. If tsc runs out of memory or the process dies before it can count anything, that is not "zero errors found," it is "we don't know," and the script fails loud rather than silently letting a broken run through as green. A ratchet is only trustworthy if it can't be gamed by the check itself falling over.
The generalization holds. Any metric on a codebase with real history, test coverage, type errors, lint warnings, bundle size, works the same way: seed the floor or ceiling from an honest measurement, fail only on regression, nudge toward tightening it, and never let the check silently pass when it actually failed to run. Absolute standards are for a codebase that starts today. Everything else needs a ratchet.