rosneri / writing
all posts
Engineering Mar 2026 · 8 min read

Zero any, one year on

Neri Rosner
Neri Rosner
backend-oriented full-stack engineer

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:

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.

Liked this? The next one is on caps: ≤400 lines per file. More writing
Keep reading DDD for people who actually ship 12 min Designing a dead-letter queue you can trust 11 min SpecBite: an honest postmortem 14 min