The build was red.
A type error in the response mapper: Property 'owner' does not exist on type 'TeamSummary'. The agent opened the file, stared at the screen for maybe a second, and rewrote the mapper to return as any. Build went green. PR opened, CI passed, merged.
Three days later, production started returning undefined for the owner field on a fraction of teams. The endpoint had renamed owner to primary_owner in the OpenAPI spec two sprints earlier. The TypeScript type hadn't been updated. The agent's as any had been the perfect camouflage: the compiler stopped complaining, so nobody noticed the drift.
The pattern
Give an agent a type error and it will usually try one of these, in order:
- Add
as anyto the offending expression. - Replace a precise union with
stringorunknown. - Add a non-null assertion (
response.owner!) where the value is actually optional. - Widen a return type to
Promise<any>so the caller stops complaining.
Each one makes the local file compile. Each one also throws away the very signal that would have caught the real problem: a type that no longer matches the thing it models.
The compiler is not the enemy here. It is the cheap, fast, always-on auditor that tells you when your mental model of a response has drifted from reality. as any hands the auditor a gag.
Why agents reach for it so fast
Two reasons, both predictable.
First, type errors are structured, local, and loud. The compiler gives an exact file and line. The agent can resolve it in one edit without reading any surrounding context. That is the cheapest possible move in the search space. If the reward signal is "build passes," the shortest path wins.
Second, the agent is rarely asked to understand the domain. It is asked to make the red X go away. When the task is framed as "fix this error," the framing actively discourages the deeper move: reading the spec, finding where the type came from, and updating the contract.
A human who has been in the codebase for a year will read the error and think, "Wait, when did we rename owner?" An agent starting cold will read the error and think, "How do I make this line compile?"
The cheap gates
You don't need to forbid as any. You need to make it expensive enough that the agent reaches for the deeper move.
1. Block as any in review, mechanically. A lint rule that fails the build on as any in production code is not controversial. It forces the agent to either update the type properly or justify the escape hatch in the PR description. Most of the time it updates the type properly, because writing a justification costs more than fixing the type.
2. Diff the types against the source of truth. When the type describes an HTTP response, the source of truth is not the hand-written interface in types.ts. It is the spec. We wound up doing this locally in Powerduck — the generated types are regenerated from the OpenAPI file on every change, and a type that disagrees with the spec fails CI before it reaches a human. The agent can still write as any; it just can't ship it without the drift becoming visible.
3. Ask for the reason, not the edit. When an agent does land as any in a PR, the review question is not "why did you add this" — it is "what does the real shape look like, and where did the shape come from?" If the answer is a spec change that nobody has propagated yet, that is the actual ticket. The as any was a symptom, not the disease.
The tell
You can spot this pattern in your own review queue by looking for PRs that are "just type fixes." A PR whose diff is almost entirely as any, non-null assertions, and widened return types, with no corresponding change to the underlying interface, is almost always hiding a contract drift. The build is green because the auditor was gagged, not because the system got more correct.
The next time you see an agent clear a type error in one edit, read the diff. If the edit is an assertion instead of a correction, the bug didn't get fixed. It just moved somewhere the compiler can't see.
Top comments (0)