TLDR: Zod (a schema validation library) guards your runtime edges. Eliminating any makes TypeScript guard everything in between. You need both.

The Project

It's a live chat app for a cancer education business — elderly cancer patients describe their situation and get matched to integrative treatment centers.

Two external data sources flow through it: TakeShape (our headless CMS) and Supabase (our Postgres database).

Both are boundaries where your types and reality can silently drift apart.

The Wall We Hit

The TakeShape import was the first sign.

Fields that should have been clean strings came through with raw HTML entities and CMS markup baked in. Then sessionToken — typed somewhere as a UUID string, somewhere else as string | null, and somewhere else just any — started silently shapeshifting depending on which path the code took.

No build error. No lint error. Things worked most of the time, which is the worst kind of bug when your users are cancer patients and their families.

The compiler wasn't catching any of it, because we'd told it not to with any.

What I Tried First

I added Zod validation schemas to every API route.

This helped — Zod at the edge means bad payloads get rejected with a clean error instead of corrupting state. Every public API route should have this. Full stop.

But it only catches drift at runtime, after deploy. The category of bug where a TakeShape field gets renamed or a Supabase row returns a different shape than expected? That still walked right through next build wearing a mask.

What Actually Fixed It

My autonomous dev-review loop flagged it directly on its eighth audit cycle: eliminate all any types.

Once they were gone and no-explicit-any was wired into ESLint, the next time a TakeShape field name drifted, the build turned red. Not at runtime. Not on the elderly patient's screen. At next build, in CI, before it ever shipped.

The sessionToken nonsense? Locked down to string | undefined. The compiler started yelling the moment any consumer tried to pass null. Which is exactly what you want.

Why This Matters

Zod and TypeScript are complementary, not either/or.

Zod says: this payload from the outside world matches the shape I expect.

TypeScript without any says: every function, every prop, every database row is the shape I said it was — and if it drifts, you hear about it at build time.

The combination means you only ship working code to the live app. Not "code that compiles and probably works." Code where the compiler has actually verified the data flow end to end.

For a production app where the user might be a 74-year-old trying to find a cancer treatment center — that difference is not academic.

Enable "strict": true in your tsconfig.json. Add "@typescript-eslint/no-explicit-any": "error" to your ESLint config. Make any a build error. Your future self will find bugs on a Tuesday afternoon instead of getting a Slack message at midnight.

P.S. If you're on Node 23.6+ and want to run TypeScript tests without a bundler — native type stripping now works out of the box, no tsx or ts-node needed. Separate story, same underlying idea: lean on the toolchain.