#5001: The 15% Bug Catch: What Type Systems Actually Deliver

Type checkers catch ~15% of bugs. Here's how to build a gradual typing ladder that actually works.

Featuring
Listen
0:00
0:00
Episode Details
Episode ID
MWP-5183
Published
Duration
23:41
Audio
Direct link
Pipeline
V5.2
TTS Engine
chatterbox-regular
Script Writing Agent
deepseek-v4-pro

AI-Generated Content: This podcast is created using AI personas. Please verify any important information independently.

The gap between knowing what a type system is and actually getting a real codebase to conform to one is wider than most developers expect. The theory is well-trodden — static versus dynamic, annotations, TypeScript, mypy. But the operational ladder from scattered annotations to a blocking CI gate to strict configs with per-module ratchets requires careful design, especially for legacy codebases that were never typed.

The dominant pattern for gradual adoption is the two-tier config. Strict mode applies to new projects and fully-typed modules, with CI blocking any untyped function. Lenient mode serves as a holding pen for legacy modules being gradually typed — existing untyped code is tolerated, but every new function added in a pull request must be fully annotated. This creates a ratchet: the untyped surface can only shrink. Sorbet's per-file sigil system — ignore, false, true, strict, strong — is the cleanest expression of this approach, allowing migration file by file, team by team.

The landmark study on type checker effectiveness comes from Gao, Bird, and Barr (2017), which found that Flow or TypeScript would have caught only 15% of public JavaScript bugs. The remaining 85% were specification errors, wrong URLs, malformed queries, and logic mismatches — things no type system can express. This is why runtime validation at trust boundaries with tools like Pydantic or Zod is essential. As Indrajeet Patil's formulation goes: parse, don't pray. Validate at the edge, then trust validated data internally, with static types as a convenience layer on top of runtime checks.

Downloads

Episode Audio

Download the full episode as an MP3 file

Download MP3
Transcript (TXT)

Plain text transcript file

Transcript (PDF)

Formatted PDF with styling

#5001: The 15% Bug Catch: What Type Systems Actually Deliver

Corn
Daniel's got a whole thing about the gap between knowing what a type system is and actually getting a real codebase to conform to one. The theory is well-trodden — static versus dynamic, annotations, TypeScript, mypy. What he wants walked through is the operational ladder. How you go from scattered annotations, to a type checker running as a blocking gate in CI, to strict configs and per-module ratchets applied gradually to a legacy codebase that was never typed, to runtime validation at the trust boundaries with something like Pydantic or Zod for the places where static types stop being able to help at all. And running underneath all of it — what conformity is actually worth. Whether a codebase that passes the checker is measurably less broken, or whether what you mostly bought was better autocomplete and an expensive ritual.
Herman
The expensive ritual question is the one that makes people flinch, because the honest answer is — it's some of both. But let's start with the ladder, because if you build it wrong you definitely get the ritual without the payoff.
Corn
Walk me through the wrong way first.
Herman
The naive approach is — annotate everything, flip on strict mode, fix all the errors, ship. Eightfold's engineering team wrote about this earlier this year. Four million lines of Python. They explicitly rejected that approach. Instagram tried it at similar scale and it took them about eight months just to hit fifty percent coverage. You burn months and you haven't even turned the checker on yet.
Corn
So the codebase is in limbo. Half-typed, no enforcement, annotations that might be wrong because nobody's checking them.
Herman
And new untyped code keeps arriving. That's the killer. Without a gate, new debt accrues as fast as old debt is paid down. I saw Protean's architecture decision record from July — they measured their codebase. About seven hundred mypy errors under a lenient config. Twenty-four hundred under strict. If you try to fix that in one pull request, it's unreviewable. If you fix it with no CI gate, you're bailing out a boat with a hole in it.
Corn
So the first rung is — stop the bleeding.
Herman
Stop the bleeding. And the dominant pattern for that is the two-tier config. You have a strict mode and a lenient mode. Strict mode — new projects, fully-typed modules. The config sets disallow untyped defs to true. CI blocks any untyped function. To onboard a module, you add its path to the strict config, run mypy locally to confirm zero errors, and merge. From that moment, no untyped code can enter that module.
Corn
And lenient mode is the holding pen.
Herman
For legacy modules being gradually typed. Disallow untyped defs is false, so existing untyped code is tolerated. But — and this is the ratchet — a separate CI check ensures every new function added in a pull request is fully annotated. The untyped surface can only shrink.
Corn
The ratchet.
Herman
It's borrowed from physical engineering. Dusty Burwell wrote about this a few years ago. A ratchet strap only moves in one direction — it spools slack onto an axle, never loosens. Applied to software, you can always make things stricter, never looser. It shows up in linter configs, test coverage targets, API versioning. For types, the cleanest expression is Sorbet's per-file sigil system.
Corn
Stripe's Ruby type checker.
Herman
Every file starts with a typed sigil. Ignore, false, true, strict, strong. Each level reports all errors from lower levels plus new ones. At strict, all methods must have signatures, all constants and instance variables must have explicit types. At strong, no T dot untyped values are allowed. The migration is file by file, team by team. You move a file up one level, it stays there.
Corn
And the escape hatch problem. Because every ratchet has an escape hatch, and the whole question is whether you can keep people from using it.
Herman
This is where it gets interesting, because the escape hatches are the same features that make gradual typing possible in the first place. TypeScript has any and as any. Python has hash type colon ignore. Sorbet has T dot untyped. The Sorbet docs are refreshingly honest about this — they say, look, at first blush it seems like you don't want this property in your type system, it lets you make wildly inaccurate claims. But it's precisely this property that enables incremental adoption. You can't do this file by file if every file has to be perfect on day one.
Corn
So the escape hatch is the feature. But then you need a second ratchet to close the escape hatch.
Herman
And that second ratchet is where the semi-programmatic decisions come in. Daniel mentioned this in the prompt — some enforcement is fully programmatic, some of it is a human deciding where the ratchet stops. The Type Ratchet GitHub action, which shipped in June, is the pure programmatic version. It counts any, as any, hash type colon ignore, and at ts-ignore occurrences in a pull request. If the count goes up, the PR fails. Zero dependency, just a counter.
Corn
The creator's rationale was specifically about AI coding agents, wasn't it?
Herman
Yeah. The argument is — AI coding agents are very good at making CI green, and the fastest route to green is as any, not a fix. A reviewer can miss one escape hatch in a four-hundred-line diff. A counter can't.
Corn
Which is a darkly funny inversion. The thing that's supposed to accelerate development creates a new category of silent type debt, so you build a robot to watch the robot.
Herman
And the robot watching the robot is four lines of bash. But not everything can be automated. There are at least three decision points where a human has to show up. First — which modules to strictify first. Eightfold deliberately started from the leaves of the dependency tree. Utility modules, data models. Annotating high-fan-out modules first maximizes the ripple effect, because every downstream consumer immediately gets better type checking.
Corn
Start where the blast radius is largest.
Herman
Second decision — what to do with Any that's legitimate. Eightfold kept disallow any expr and warn return any off even in strict mode, because in practice Any is unavoidable in a large codebase with third-party libraries and untyped dependencies. A pandas Series of Any is just what it is. The Type Ratchet action lets you set a baseline any parameter for those legitimate uses.
Corn
And the third one is the one I think is actually the hardest. Whether a given type ignore is debt or permanent.
Herman
The convention that's emerging in several codebases is that every type ignore must include a reason comment. Type ignore attr-defined, third-party lib missing stubs. With warn unused ignores set to true, mypy tells you when the annotation finally makes the ignore unnecessary. The ignore rots on the vine and you prune it.
Corn
There's also the annotation review loop. The thing where you run a production type tracer and it gives you back types that are technically correct and completely wrong.
Herman
MonkeyType. Eightfold used it. You run it in production, it records the actual types that flow through your functions, and it generates annotation suggestions. The problem is — runtime tracing produces concrete types, not abstract ones. A utility function that works on any list type will produce a union of every concrete type seen in production. List of int, list of str, list of dict, all unioned together. A human has to come in and replace that with a proper TypeVar generic.
Corn
So the machine gives you raw material and you still need someone who understands the intent of the code.
Herman
The machine tells you what happened. You have to decide what should happen. That's not automatable.
Corn
Let's talk about the quarantine model versus the two-tier model. Because they get to the same place but they feel different.
Herman
Protean chose quarantine. All modules start failing, then get individually silenced and re-enabled. Eightfold chose two-tier — modules opt into lenient or strict. The quarantine model is more aggressive. CI blocks immediately, even for untyped modules. The two-tier model is more permissive — untracked modules produce zero noise. The quarantine model says we're going to feel the pain now and ratchet it down. The two-tier model says we're going to expand the zone of safety outward.
Corn
Which one you pick says something about your organization.
Herman
Quarantine requires a team that can tolerate a noisy CI for a while and not learn to ignore it. Two-tier requires discipline to actually move modules into the tiers. The failure mode of quarantine is alert fatigue. The failure pattern of two-tier is — the strict tier stays at three modules forever.
Corn
Alright. So we've climbed the ladder. We've got per-module ratchets, we've got CI gates, we've got human decision points for the places the ratchet can't reach. What's at the top? What does a codebase that passes the checker actually buy you?
Herman
The landmark study on this is Gao, Bird, and Barr from twenty seventeen. They looked at public JavaScript bugs and asked — how many would Flow or TypeScript have caught? The answer is fifteen percent. With a ninety-five percent confidence interval of eleven and a half to eighteen and a half.
Corn
Fifteen percent.
Herman
Fifteen percent of public bugs. And that's conservative, because it only measures bugs that survived testing and review — it understates effectiveness during private development. But still. Fifteen percent.
Corn
So more than eight out of ten bugs walk right past the type checker.
Herman
More than eight out of ten. And the study breaks down what the other eighty-five percent are. Fifty-five percent were specification errors — failures to implement the spec. Wrong URLs, malformed queries, logic that doesn't match what the product was supposed to do. A type system can't express "this URL is correct." String errors were the second category. These are fundamentally beyond what any type system can catch.
Corn
Which is the point Daniel's making about Pydantic and Zod. The static checker stops at the boundary.
Herman
And the boundary is where the real danger is. Every byte that crosses your service boundary is hostile until proven otherwise. HTTP request bodies, external API responses, environment variables, message-queue payloads — these are all unvalidated data with no type guarantee. Static types help you write the code in between, but they don't guarantee the data is correct.
Corn
The phrase I've seen is — parse, don't pray.
Herman
Indrajeet Patil's formulation. And the reason to define the shape in Pydantic or Zod rather than in raw JSON Schema is not ergonomics. It's that the schema you send, the validator you run, and the type your editor knows about become the same object. They can't drift apart. Written by hand, those are three files that agree today. Derived from one definition, disagreement is impossible.
Corn
So you validate at the edge, and then you trust the validated data internally. The static types are a convenience layer on top of the runtime check, not the other way around.
Herman
Steven's Knowledge describes the architecture explicitly. There is exactly one place to validate untrusted input — the moment it enters your system, before any business logic touches it. Not in the database layer, not scattered through services. Pydantic's extra forbid and Zod's strict object prevent mass-assignment attacks. Size and range limits prevent denial of service through memory exhaustion. The static types are for the code you control. The runtime validators are for the data you don't.
Corn
So that's the ladder. But I want to go back to the fifteen percent, because I think that number is doing more than one thing.
Herman
It is. Because there's a counterintuitive finding that's come out of subsequent work. Bogner and Merkel looked at six hundred and four GitHub projects in twenty twenty-two. TypeScript applications have a higher bug-fix commit ratio than JavaScript applications. Zero point two zero six versus zero point one two six. And longer bug-fix time — thirty-three days versus thirty-two.
Corn
Wait. Typed codebases have more bug fixes?
Herman
More bug-fix commits. And a twenty twenty-five replication by Kuznetsova and Cherkasov confirmed it. TypeScript showed thirty-four percent fewer code smells, twenty-eight percent lower cognitive complexity — and a thirty-two percent higher bug-fix commit ratio and ten percent longer bug-fix time.
Corn
So the code is cleaner, easier to reason about, and you spend more time fixing bugs.
Herman
The plausible explanation is — TypeScript catches many defects at compile time that would otherwise go unnoticed. The type annotations make bugs easier to diagnose when they do occur. So you're seeing more bug-fix activity not because there are more bugs, but because you're actually finding and fixing bugs that were always there.
Corn
The metric is measuring detection, not defect introduction.
Herman
And that means teams adopting types should expect to see more type-related CI failures initially, not fewer. If your bug-fix commit ratio goes up after adopting types, that's the system working. You're catching things you were shipping before.
Corn
Which is a hard sell to a manager who sees the CI going red more often.
Herman
The Microsoft engineering manager quoted in the Gao study had the right framing. If you could make a change to the way we do development that would reduce the number of bugs being checked in by ten percent or more overnight, that's a no-brainer. Ten percent. Not fifty. Not ninety. Ten percent, and it's worth it.
Corn
And yet the annotation tax is real. The time cost of writing and maintaining types has not been rigorously measured at scale in the same experiment. We know what we catch. We don't know what we paid.
Herman
We don't. And the Bogner and Merkel finding that TypeScript projects have longer fix times is consistent with the idea that type checking creates more work. Whether the net effect is positive depends on how you value the bugs caught versus the time spent. There's no dollar figure for this.
Corn
So what does the person maintaining an untyped codebase, being told to adopt this, actually do on Monday morning?
Herman
Monday morning, you install the type checker in CI with a per-module quarantine list. Every module that currently has errors gets ignore errors equals true. The quarantine list is append-only shrinking. You pick one module — the leafiest, highest-fan-out module you can find — and you annotate it. You move it to the strict tier. You do this once a sprint. In six months, half your codebase is strict.
Corn
And you don't let the AI write as any to make the light green.
Herman
You run the counter. The counter is four lines. It costs you nothing and it catches the thing the reviewer will miss.
Herman
There's one more thing I want to put on the table, because it complicates the "is it worth it" question in a way that's specific to this moment. AI coding agents can generate type annotations faster than humans. That should accelerate the ladder. But the Type Ratchet creator's point is — they also reach for as any and type ignore as the fastest path to green. So you get more annotations, faster, and some fraction of them are fake.
Corn
The speed makes the cheating harder to spot.
Herman
Tomoda Hinata wrote about this earlier this year. The argument is — what makes an AI coding agent's speed safe is not human review but mechanical quality gates. The linter, the type checker, the test suite. The machine reviews the machine. And the type ratchet tools were built specifically for this world. Type Ratchet, Test Ratchet, Suppress Ratchet — they're all counters. They don't understand your code. They just count the escape hatches and fail the PR if the number goes up.
Corn
The ladder is the same ladder. The AI just means you climb it faster, and you need better guardrails.
Herman
The guardrails are simpler than the AI. That's the part I find satisfying. The counter is a bash script. The type checker is a deterministic program. The AI generates a thousand lines and four small programs check its work.
Corn
Alright. Let's land the question Daniel actually asked. What does each rung buy you?
Herman
Rung one — scattered annotations with no CI gate. Buys you better autocomplete. That's it. But that's not nothing. Multiple practitioners report the IDE experience improvement is the benefit most immediately felt by developers, even before any production bugs are caught.
Corn
Rung two — CI gate with lenient mode. The ratchet is on.
Herman
New code is typed. Old code is tolerated but can't get worse. You've stopped the bleeding. The bug detection starts here — not at fifteen percent yet, because coverage is low, but you're catching type errors in new code at review time instead of in production.
Corn
Rung three — per-module strict mode. Modules are flipping to strict one by one.
Herman
The high-fan-out modules are fully checked. Downstream consumers get better type inference. The fifteen percent number starts to become real for the modules under strict. And you get the null-checking benefits — TypeScript's strict null checks alone increased detectable bugs by fifty-eight percent. That's the single highest-return flag you can flip.
Corn
Rung four — runtime validation at the boundaries.
Herman
Pydantic, Zod. This is where you catch the eighty-five percent that static types can't touch. Malformed data, wrong URLs, missing fields, mass-assignment attacks. The static types handle the code you wrote. The runtime validators handle the data you didn't.
Corn
The top rung — the question of whether the whole thing was worth it.
Herman
You've bought fifteen percent fewer public bugs, better autocomplete, faster navigation, and a codebase where the machine can tell you when you've made a mistake before the user does. You've also bought an annotation maintenance burden that nobody has quantified in dollars. The honest answer is — it's worth it if you flip strict null checks and validate at the boundaries. If you stop at scattered annotations and never turn on the CI gate, you bought the ritual without the payoff.
Corn
The autocomplete is real but it's not why you did all this work.
Herman
The autocomplete is the gateway drug. It gets people to write annotations. The checker is what makes those annotations mean something.

Hilbert: Fifteen percent.
Herman
What?

Hilbert: The number you kept coming back to. Fifteen percent of public bugs. I spent three years testing telephone switching software, late eighties, and our target was twelve percent. Twelve percent of faults caught at the integration test stage was considered a good yield. We had a whole quality metric built around it. Fifteen would have been exceptional.
Corn
What kind of switching software?

Hilbert: Stored program control exchanges. The kind where a software fault doesn't crash a server, it takes forty thousand phone lines dark. We had a test harness that ran for eleven hours. It caught about one fault in eight. The rest we found in the field.
Herman
The fifteen percent number doesn't sound low to you.

Hilbert: It sounds familiar. The type checker is your eleven-hour test harness. It catches the category of fault it's designed to catch. The rest are specification errors, timing errors, resource exhaustion — things no static analysis was ever going to find. We didn't conclude the test harness was an expensive ritual. We concluded twelve percent was the shape of the problem.
Corn
The ones you found in the field — how bad were they?

Hilbert: We had a fault once where a memory leak in the call-routing module would degrade a switch after about six weeks of uptime. The fix was a single line. Finding it took four months and cost more than the development budget for the entire module. That's the kind of thing a type checker doesn't catch either, by the way. The code was perfectly well-typed.
Herman
Resource leaks are a different category entirely.

Hilbert: My point is — fifteen percent is not a small number. People hear fifteen percent and think failure. I hear fifteen percent and think — that's the difference between a switch that stays up for six weeks and a switch that stays up indefinitely, if the fifteen percent includes the memory leak. It doesn't, but you take the point.
Corn
The categories don't overlap perfectly between type checking and integration testing.

Hilbert: No, and that's what I'm saying. You stack them. The type checker catches its fifteen percent. The test harness catches its twelve. The runtime validators catch another chunk. None of them is the whole answer. The question isn't whether fifteen percent is enough. The question is whether you can afford to leave fifteen percent on the table.
Herman
The Microsoft manager's argument exactly. Ten percent is a no-brainer.

Hilbert: Ten percent of faults in a telephone exchange is the difference between a quiet Tuesday and a front-page story. Different stakes, same arithmetic. I'll tell you what's an expensive ritual — the four months we spent chasing that memory leak. The type annotations take a sprint. The leak took a quarter.
Herman
You can't know in advance which fifteen percent you're catching.

Hilbert: You never know. That's the whole problem with quality metrics. You only know what you found, never what you prevented. But I kept a list, for a while, of faults we caught in the harness. Every one of them would have been a customer-affecting outage. After about forty entries I stopped keeping the list. It was making me nervous.
Corn
The list was making you nervous?

Hilbert: The list was making me aware of how many ways the thing could break. I preferred not to think about it. The harness ran, the light went green, I went home.
Herman
That's the CI gate in a nutshell.

Hilbert: That's the CI gate. You don't want to know. You want the light.
Herman
We should probably wrap. The thing I keep thinking about is — the ladder Daniel described is real, it's well-understood, the patterns are documented. Eightfold, Protean, Stripe — they've all climbed it and written down what they learned. The open question isn't how to climb it. It's whether AI-generated code changes which rungs matter most.
Corn
The counter becomes more important than the checker.
Herman
Or the counter becomes the checker. The thing that enforces the ratchet isn't the type system itself, it's the meta-tool that watches the escape hatches. Which is a weird place for the industry to land — the most important tool in your type safety strategy is a script that counts hash type ignore.
Corn
The ladder ends in a bash script. That feels right, somehow.
Herman
Thanks to Hilbert Flumingtop for producing. This has been My Weird Prompts. If you want to tell us about your own type adoption war stories, email us at show at my weird prompts dot com. We'll be back soon.

This episode was generated with AI assistance. Hosts Herman and Corn are AI personalities.