
TypeScript was supposed to save us. No more "undefined is not a function." No more runtime type errors. Catch bugs at compile time. Developer experience nirvana. We mandated TypeScript for every JavaScript project and started migrating our Python services to typed Python with mypy.
Two years later, we reversed course. Not because types are bad—they're not—but because we'd turned typing into a religion instead of a tool. The overhead was eating our velocity, and the bugs we were catching weren't the bugs that mattered.
This is the story of our TypeScript maximalism, why it failed, and what we do instead. Spoiler: types are great for some things and overkill for others. The trick is knowing which is which.
The TypeScript Mandate
In 2022, we made TypeScript mandatory for all new JavaScript/Node.js projects. Existing JavaScript codebases had one year to migrate. Python projects were required to use mypy with strict settings. No exceptions.
The reasoning seemed solid:
- Type safety: Catch bugs before they reach production. The compiler becomes a first line of defense.
- Documentation: Types serve as living documentation. No more guessing what a function expects or returns.
- Refactoring confidence: Change a type and the compiler tells you everywhere that's affected. Safe, large-scale changes.
- IDE experience: Better autocomplete, better navigation, better developer experience.
We hired a "TypeScript champion" to lead the migration. We created training materials. We established strict linting rules. We were doing this right.
The Overhead We Didn't Anticipate
Six months into the mandate, patterns emerged that we hadn't anticipated. TypeScript wasn't free—it had costs that compounded as codebase complexity grew.
Type Gymnastics
Some things are trivial to do in JavaScript and nightmarish to type in TypeScript. Consider a simple utility function that merges objects with optional overrides:
// JavaScript: 3 lines, obvious, works
function merge(defaults, overrides) {
return { ...defaults, ...overrides };
}
The TypeScript version that properly types this—handling optional properties, preserving property types, and dealing with undefined values—is significantly more complex. We had engineers spending hours on type signatures for what should be simple utility functions.
And it got worse. API response handling, form validation, state management—all required elaborate type definitions. We created types, then types of types, then utility types to manipulate other types. The type system became a programming language of its own, with its own bugs and complexity.
We did a time study. Across representative sprints, engineers spent 25-30% of development time on type-related work: writing types, fixing type errors, researching how to type complex patterns, debating type design in code reviews. That's a nearly one-third velocity tax.
The any Escape Hatch
When typing got too hard, engineers used any. It was the path of least resistance. "I'll come back and fix it later" (they never did).
We audited our codebases. One service had 847 uses of any. Another had 1,200. The TypeScript mandate had created the illusion of type safety while large portions of the code had no type checking at all.
We tried banning any with lint rules. Engineers started using unknown with immediate casts, or created empty interfaces that were functionally equivalent to any. The workarounds proliferated. We were playing whack-a-mole with escape hatches.
Type Definition Maintenance
Every API change required type definition updates. External APIs needed their own type definitions, which drifted out of sync with the actual API behavior. Library upgrades broke type definitions even when the runtime code worked fine.
We had a production incident where a deploy was blocked for 6 hours because a library upgrade changed their type definitions in a breaking way—even though the actual functionality was compatible. The types were preventing us from shipping working code.
Compilation Time
TypeScript compilation adds time to every build and every test run. For small projects, it's negligible. For our larger services, type checking added 45-90 seconds to the build. Running the type checker in watch mode consumed 2GB of RAM.
Engineers started skipping type checks during development to move faster, only running them before committing. Which meant they'd write code for an hour, then spend 30 minutes fixing type errors at the end. The feedback loop was broken.
The Bugs We Were Actually Catching
The promise of TypeScript is catching bugs at compile time. So we analyzed: what bugs was TypeScript actually catching for us?
We reviewed six months of PRs, identifying commits where TypeScript errors had caught real bugs (not just type definition issues). The results:
- Typos in property names: ~40% of catches. "Oops, I typed 'usrId' instead of 'userId'." Helpful, but most of these would have been caught by the first test run anyway.
- Missing null checks: ~25% of catches. Genuine value. TypeScript's strict null checking found places we forgot to handle undefined values.
- Wrong argument types: ~20% of catches. Passing a string where a number was expected. Often the code would have crashed immediately when tested.
- Structural mismatches: ~15% of catches. API response didn't match expected shape. Valuable when refactoring, less valuable for new code.
What bugs was TypeScript NOT catching?
- Logic errors: The code has the right types but does the wrong thing. TypeScript can't help here.
- Race conditions: Async code that works most of the time but fails under load.
- Edge cases: Input that's the right type but the wrong value (negative numbers, empty arrays, malformed strings).
- Integration issues: Service A and Service B have compatible types but incompatible interpretations of what those types mean.
The bugs that caused actual production incidents? Logic errors and integration issues. The categories TypeScript doesn't help with. The bugs TypeScript caught were mostly the kind we'd find in the first 5 minutes of testing.
The Python Comparison
We had an interesting natural experiment. Some teams used Python with strict mypy. Other teams used Python without type annotations (legacy codebase that never got migrated).
We compared bug rates between the typed and untyped Python services:
| Metric | Typed Python (mypy strict) | Untyped Python |
|---|---|---|
| Production incidents/month | 2.3 | 2.1 |
| P1 bugs/quarter | 4 | 5 |
| Average time to ship feature | 12 days | 8 days |
| Test coverage | 74% | 82% |
The untyped Python services had slightly fewer production incidents. The difference wasn't statistically significant—but it certainly wasn't the disaster that type advocates predicted.
What explained this? The untyped Python teams, knowing they didn't have a type checker safety net, wrote more comprehensive tests. They tested edge cases more thoroughly. They caught bugs through testing that the typed teams expected the type checker to catch.
The typed Python teams had lower test coverage. They trusted the type checker. But the type checker couldn't catch logic bugs—which are the bugs that matter.
The Realization
We had built a false dichotomy: typed = safe, untyped = dangerous. The reality was more nuanced.
Types are good at:
- Documenting interfaces between components
- Enabling IDE autocomplete and navigation
- Catching typos and structural mismatches during refactoring
- Expressing domain constraints when the type system is powerful enough
Types are not good at:
- Catching logic errors
- Replacing comprehensive testing
- Making code correct (they can validate shape, not meaning)
- Justifying the overhead for short-lived or rapidly-changing code
We'd been using types as a substitute for testing and design thinking. "It type-checks, so it's correct." That's not how type systems work.
The New Policy: Strategic Typing
We replaced the mandate with guidelines. Types are now a tool to be used consciously, not a religion to be followed blindly.
Where We Use Strong Typing
Shared libraries: Code used across multiple services benefits from typed interfaces. The consumer can't see the implementation, so types serve as the contract. Library code tends to be stable, so the upfront typing investment pays off.
Long-lived services: Services that have been running for years and will run for years more benefit from types. The codebase is large, many engineers touch it, and refactoring happens. Types help navigate complexity and enable safe changes.
API contracts: Request and response types for APIs are well-worth defining. They catch integration bugs early and serve as documentation for consumers.
Complex domain logic: When business rules are intricate, types can encode constraints. Positive numbers, non-empty strings, valid state transitions—types can make illegal states unrepresentable.
Where We Skip Strong Typing
Scripts and glue code: One-off scripts, data pipelines, and integration glue don't benefit from typing overhead. They're written once, run, and often discarded. Typing them is pure overhead.
Prototypes and experiments: Code that might be thrown away next week shouldn't have typing tax. Explore fast, type later if the code survives.
Test code: Typing test files often adds friction without catching useful bugs. Tests are already verified by whether they pass. We allow looser typing in test directories.
Small, focused services: A microservice with one job and one team doesn't need elaborate types. The whole thing fits in your head. Types add overhead without corresponding benefit.
Practical Guidelines We Use
The "3 engineers or 3 months" rule: If a codebase is touched by 3+ engineers or will be maintained for 3+ months, invest in types. Otherwise, probably not worth it.
Type the boundaries, not the internals: Public interfaces (APIs, library exports) should be strongly typed. Internal functions can be looser. This gets 80% of the benefit for 20% of the effort.
Prefer simple types: If your type definition is longer than the function it describes, something is wrong. Simplify the design or accept unknown with runtime validation.
Tests over types for logic: Types verify shape. Tests verify behavior. Don't rely on types to catch bugs that tests should catch.
Runtime validation at trust boundaries: External input (API requests, file uploads, third-party data) should be validated at runtime with libraries like Zod or io-ts, not trusted because it matches a type definition.
The Results
Six months after relaxing the mandate:
- Feature velocity: Up 15-20%. Less time on type gymnastics, more time on features.
- Bug rate: No significant change. We worried bugs would increase. They didn't.
- Developer satisfaction: Up significantly. Engineers who love types can still use them. Engineers who found types frustrating are relieved.
- Test coverage: Up 8 percentage points. Teams without type checker safety nets write more tests.
The feared disaster—production burning because we removed types—never materialized. Types were never preventing the bugs that actually caused incidents.
The Uncomfortable Truth
TypeScript advocates often imply that untyped code is irresponsible—that "real" engineers use types. This is ideology, not engineering.
The best-known technology companies use a mix of typed and untyped languages. Facebook invented Flow (typed JS) but also uses Python and PHP extensively. Google uses Go (typed) and Python (often untyped). Stripe uses Ruby (dynamic) alongside TypeScript.
Types are a tool. Like all tools, they have appropriate and inappropriate uses. A hammer is great for nails and terrible for screws. Using TypeScript everywhere is like insisting on hammers for everything.
When To Type, When Not To
Ask yourself before adding types:
- How long will this code live? Days/weeks → probably skip typing. Years → probably worth typing.
- How many people will touch it? Just me → loose typing is fine. Many engineers → typing helps coordination.
- How complex are the interfaces? Simple CRUD → types are overkill. Complex domain → types help.
- What bugs am I trying to prevent? Shape mismatches → types help. Logic errors → tests help.
- What's the typing overhead for this pattern? If you're fighting the type system, maybe the type system isn't the right tool.
Answer these questions honestly. Sometimes the answer is "yes, types." Sometimes it's "no, not worth it." Both are valid engineering decisions.
Types are valuable. Type fanaticism is expensive. The best engineers know when to type and when to ship. Mandating types everywhere is as misguided as banning them everywhere.
Written by XQA Team
Our team of experts delivers insights on technology, business, and design. We are dedicated to helping you build better products and scale your business.