Zero any, one year on
Banning any from a TypeScript codebase sounds like a linting nicety — a box you tick
in tsconfig and forget about. In practice it became the single biggest
forcing-function on how I structure code.
I made the rule a year ago, half as a dare to myself: no any, no unchecked casts, no @ts-ignore without a written reason. Every escape hatch is a compile error, not a warning. The compiler is the first reviewer on the team, and it does not negotiate.
The rule, exactly
Three settings do most of the work, and one social contract does the rest:
strict: true— table stakes, but people still ship without it.noImplicitAnyand a lint rule that bans explicitanytoo.- No type assertions across a boundary you don’t own. Parse, don’t cast.
That last one is where the architecture lives. The moment you can’t paper over an unknown shape, you have to name it — and naming it forces the question of where the shape becomes trustworthy.
// boundary.ts
// not allowed — the boundary is a lie
const data = (await res.json()) as User;
// required — parse at the edge, trust the type after
const data = User.parse(await res.json());
// ^? User — validated, not asserted
What it forced
Once unknown data has to be parsed before it enters the system, types migrate to the boundary on their own — the HTTP handler, the queue consumer, the database row — and the core stays honest. Validation collects at the edges; the inside becomes a place where every value already means what it says.
The result looks a lot like domain-driven design. I didn’t set out to do DDD. The compiler walked me there, one rejected any at a time.
If a type is hard to write, the design is usually wrong. The friction is the signal — don’t suppress it, follow it.
The cost, honestly
It is slower on day one and faster every day after. The genuinely annoying case is a third-party SDK with bad types. I don’t fight those inline — I quarantine them:
// vendor.d.ts
// one file. reviewed. never spreads into the domain.
declare module "sketchy-sdk" {
export function connect(url: string): Client;
}
One file holds the mess, explicit and contained. Everything downstream of it gets a real type. The cost is bounded; the benefit compounds.
Was it worth it?
The compiler caught three production-grade bugs before code review did — a null that wasn’t handled, a currency parsed as a number, an enum that grew a case nobody updated. The fourth bug I shipped anyway. That one was a logic error, not a type error, and no tsconfig setting saves you from being wrong about the world.
A year in, zero any isn’t a constraint I fight. It’s the cheapest senior engineer on the team — tireless, humorless, and right often enough that I’ve stopped arguing.
Filed under engineering. Disagree? Tell me why — I reply to most of it.