Denshin / Blog / Engineering
Our typecheck script checked nothing
In our Turborepo monorepo, tsc --noEmit ran against a solution style tsconfig with an empty files list, checked zero files and passed every time. A short note on project references and on proving a check can fail.
Denshin Engineering · Engineering Team · 24 October 2026 · 3 min read
For a good while, the typecheck script in our web app passed every single time. That should have been the clue.
The repo is a Turborepo monorepo on Yarn 4 workspaces: the website and admin panel, the API, and a few shared packages. The web app's package.json said:
"typecheck": "tsc --noEmit"
Looks right. The catch is the tsconfig.json it runs against. The Vite React template sets up a solution style config, where the root file lists no sources of its own and points at two other configs, one for the app and one for the Vite config:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
Plain tsc does not follow project references. It reads "files": [], type checks exactly zero files, finds exactly zero errors, and exits with code 0. Turborepo ran it in every workspace and reported a clean result each time.
You can see it for yourself in about ten seconds. Add --listFiles to the command and look at the output. On a config like this one it lists nothing at all, which is about as clear a confession as a compiler can make. Run the same flag through tsc -b --verbose and you get the opposite: every referenced project, which files are out of date, and what it is checking.
To check referenced projects you need build mode, tsc -b. Our build script was already running that. So real type errors were caught, but later, at build time, by a different command, while the command named after the job reported success.
We noticed in September 2026, during the smooth scrolling work, when yarn typecheck and the build disagreed about whether the code was fine. Since then we run tsc -b when we want a type check. The fix to the script itself is one line: make typecheck run tsc -b too, so the name and the behaviour agree.
A check you have never seen fail
The habit we took from it is simple. A check you have never seen fail is not yet a check. When we add a lint rule, a test runner, a type check or a CI gate, we try to break something on purpose once and watch it go red. A deliberate type error. An assertion that cannot pass. An unused import. If the tool stays green, it is not looking where you think it is looking.
The same shape turns up outside tooling. Health checks that only confirm the process is running. Alerts on a metric nobody emits any more. Backups nobody has tried to restore. All of them are green, quiet, and measuring nothing, right up until the day you need them. We have a related rule about AI generated changes in our notes on coding agents: a passing check only means something if the check could have failed.
In hindsight there was a visible symptom the whole time. A real type check on a project this size takes a noticeable few seconds. Ours finished instantly.
Tags: TypeScript, Monorepo, Turborepo, Tooling, War Story
All posts · Work with Denshin