Three years ago we had a mix of TypeScript and plain JavaScript across our client projects. The split was roughly 60/40. Today it's 100/0. Not because someone wrote a memo declaring TypeScript mandatory, but because every project that started untyped eventually hit the same wall, and the cost of hitting that wall exceeded the cost of typing from the start. The math just stopped working.
This isn't a TypeScript evangelism post. We're not going to tell you that types are beautiful or that the type system is Turing-complete and therefore magical. We're going to tell you what actually happened on our projects, what we measured, and what two rules came out of it.
§ 01The real cost of "we'll add types later"
The most common thing we heard from founders in 2023 was some version of "we'll move fast in JavaScript now and add types when things stabilize." This sounds reasonable. It is not. Here's why: things don't stabilize. Products that are growing change constantly — new endpoints, new data shapes, new integrations. The moment when the codebase is calm enough to retrofit types never arrives.
What does arrive is the bug that takes three days to find because a field changed from string to string | null in an API response and nobody noticed. Or the refactor that touches forty files and breaks in production because the developer renamed a property but missed two call sites that only execute on Tuesdays. These are not hypothetical. We tracked incidents across eight client projects in 2024, and 41% of production bugs in untyped codebases were shape mismatches — the function received something different from what the developer assumed.
In the typed codebases, that category of bug was essentially zero. Not reduced. Zero. The compiler caught every single one before it shipped.
§ 02What changed our minds permanently
The project that flipped the switch was a fintech dashboard we built in mid-2024. The client wanted to move fast, so we started in plain JavaScript. By week six, the API contract between the frontend and the backend had drifted so far that we were spending more time debugging integration mismatches than building features.
We stopped on a Thursday, spent Friday and the following Monday converting to TypeScript with strict: true, and the compiler surfaced 23 latent bugs that we hadn't found yet. Twenty-three. Some of them were in code paths that only triggered for edge-case users, meaning they would have hit production quietly and eroded trust over months.
The compiler found twenty-three bugs in two days. Our test suite had found zero of them in six weeks.
After that project, we made the call. Every new engagement starts with TypeScript. Every shared package is typed. Every API contract is defined as a type before the first endpoint is written. There is no "add types later" option on the menu.
§ 03The two rules we enforce
We don't have a fifty-page TypeScript style guide. We have two rules. They cover about 90% of the value we get from the type system:
Rule 1: No any, ever, except in test fixtures. The any type is a hole in the safety net. One any in a function signature propagates through every caller. We use unknown when the type genuinely isn't known, and we narrow from there. The only exception is test fixtures where mocking deep third-party objects would add noise without catching real bugs. Every other any is a code review rejection.
Rule 2: Types own the contract, not the implementation. We define types for every API response, every database query result, every message passed between services. The type is written first, committed first, reviewed first. Implementation follows. This means the type is a spec, not documentation-after-the-fact. When someone reads the type, they're reading what the system actually does, not what someone hoped it would do six months ago.
// The type IS the spec. Written before the endpoint exists. type InvoiceResponse = { id: string; status: "draft" | "sent" | "paid" | "void"; lineItems: ReadonlyArray<LineItem>; total: { amount: number; currency: string }; paidAt: string | null; }; // Implementation must satisfy this shape — compiler enforces it.
These two rules are simple enough that a new developer joining the project understands them in five minutes. They're strict enough that they prevent the entire class of bugs that used to eat our debugging hours.
§ 04How strict is strict enough
We run strict: true in every tsconfig.json. That's the baseline. On top of that, we enable noUncheckedIndexedAccess, which forces you to handle the possibility that an array or object access might return undefined. This one flag alone catches a surprising number of off-by-one errors and missing-key bugs.
We also run exactOptionalPropertyTypes on newer projects. It distinguishes between "this property is missing" and "this property is present but set to undefined." The distinction matters when you're serializing to JSON or passing objects to APIs that treat missing fields differently from null fields.
Where we stop: we don't chase type-level programming for its own sake. We've seen codebases where the types are so clever that only the person who wrote them can read them. A type that takes five minutes to understand is a type that's too complex. We flatten, we simplify, we duplicate types across modules if the alternative is a generic that requires a PhD to parse.
A type that takes five minutes to understand is a type that's too complex.
— Internal style guide, rule of thumb
§ 05When we skip types on purpose
There are exactly three cases where we relax:
- Throwaway scripts. A one-off data migration script that runs once and gets deleted? Plain JavaScript is fine. It's going to live for forty-five minutes. Typing it would take longer than writing it.
- Rapid prototyping in the first two hours. When we're spiking a concept to see if an approach is viable, we sometimes start untyped and convert once the spike succeeds. But the spike never ships. If the concept graduates to production code, it gets typed before it merges.
- Third-party library glue. Some libraries have incomplete or incorrect type definitions. Rather than fighting the type system with elaborate workarounds, we'll create a thin wrapper with a well-typed public interface and allow minimal
anyusage inside the wrapper. The boundary stays clean; the mess is contained.
Every other case: typed from line one.
§ 06The tooling payoff nobody talks about
The bug-prevention story is the obvious win. The less obvious win is what types do to every other tool in the chain. When your codebase is fully typed, your IDE becomes dramatically more useful. Autocomplete works. Rename-symbol actually finds every reference. Go-to-definition doesn't guess — it knows.
Code review gets faster because reviewers can trust the type signatures. If a function says it returns Invoice | null, the reviewer doesn't have to trace through the implementation to verify. The compiler already did. That shaves minutes off every review, and across a team of five reviewing four PRs a day, those minutes compound.
AI-assisted coding also improves measurably. When the types are precise, LLM-generated code is more likely to be correct on the first pass because the model has an unambiguous specification to work against. We've seen a noticeable reduction in back-and-forth when the types are solid versus when the model is guessing at shapes.
Refactoring is where the payoff is largest. On an untyped codebase, a significant refactor — renaming a concept, splitting a module, changing a data shape — is terrifying. You write the change, run the tests, and pray. On a typed codebase, you make the change to the type and the compiler gives you a list of every file that needs to update. It's not a prayer. It's a punch list.
We don't think TypeScript is perfect. The build step adds complexity. The type system has sharp edges. Generic inference sometimes does things nobody expects. But the tradeoff — a few minutes of type annotations per function in exchange for an entire category of bugs eliminated and a fundamentally better development experience — is not close. It's the most one-sided tradeoff in modern frontend engineering.
Type from day one. The math works.
— End of essay. If you're starting a new project or converting an existing one, we can help. Start a project →