I mass-adopted TypeScript about three years ago. Moved an entire codebase over. Took six weeks. My team and I were excited about it. We’d finally have type safety. We’d catch bugs before they hit production. We’d write more reliable code.
That was the pitch anyway.
Three years in, I want to talk about what actually happened. Because it’s not what the TypeScript evangelists told me would happen. And I think a lot of JavaScript developers are being sold something without hearing the full story.
How we got here
Our codebase was about 80K lines of JavaScript. Node backend, React frontend. Normal stuff. We had the usual problems. Occasional runtime errors that slipped past tests. New developers joining and struggling to understand function signatures. Objects getting passed around with unclear shapes.
TypeScript was supposed to fix all of that.
My tech lead at the time was a huge advocate. He’d been using it on side projects and he made it sound like the answer to every problem we’d ever complained about. Types would be our documentation. The compiler would be our safety net. Refactoring would be painless.
We voted as a team. It wasn’t unanimous but the majority wanted to try it. So we committed.
First two weeks felt great. We started with the backend. Adding types to existing functions was almost fun. You could see the shape of the data flowing through the system. Things clicked into place.
Then week three happened.
The migration that ate our quarter
Converting 80K lines of JavaScript to TypeScript is not a two week project. It’s not a six week project either, even though that’s what we estimated.
It took us four months.
The first problem was third party libraries. Half our dependencies didn’t have good type definitions. Some had DefinitelyTyped packages that were outdated. Some had none at all. So we’d hit a wall, spend half a day writing custom type declarations for a library we didn’t own, and then move on.
That happened constantly.
The second problem was our own code. JavaScript is flexible by design. We’d written functions that accepted strings sometimes and objects other times. We had utility functions that returned different types depending on what you passed in. Totally normal JavaScript patterns. Totally painful to type correctly.
We ended up using any way more than anyone wants to admit. I looked at our codebase last week. We still have 317 instances of any. Three years later.
The third problem was that the migration itself introduced bugs. Not type errors. Real bugs. Because when you’re rewriting function signatures across hundreds of files, you occasionally change behavior by accident. We had two production incidents during the migration that were directly caused by the conversion process.
We were adding TypeScript to reduce bugs. The process of adding it created bugs. That irony was not lost on us.
The daily tax
Here’s the thing nobody mentions when they’re selling you on TypeScript. The migration is a one-time cost. The daily tax is forever.
I timed myself over two weeks last month. Just roughly tracking where my time went. Here’s what I found.
I spent about 40 minutes per day dealing with TypeScript-specific friction. Not writing types for new code. That part is fine and usually quick. I’m talking about:
Fighting the compiler when I know the code is correct but TypeScript disagrees. Writing generic types for utility functions that would be three lines in JavaScript but need fifteen lines of type gymnastics in TypeScript. Debugging type errors that have nothing to do with actual bugs.
Forty minutes a day. Five days a week. That’s over three hours a week spent on type system friction.
Across a team of six engineers, that’s roughly 18 hours of engineering time per week. On types. Not on features. Not on bugs. On satisfying the compiler.
I brought this up to my manager once. He said “but think about the bugs you’re preventing.” I wanted to agree with him. But honestly I don’t think 18 hours of weekly type wrangling is preventing 18 hours worth of bugs.
The complexity ceiling
This is the part that really gets me. TypeScript was supposed to make our code simpler to understand. More readable. Self-documenting.
For basic stuff, it does. A function that takes a string and returns a number? Great. Crystal clear. Anyone can read that.
But our codebase doesn’t have many functions that take a string and return a number.
We have functions that accept a configuration object with twelve optional fields, three of which change the return type. We have higher order functions that wrap API calls with retry logic and caching. We have React components with props that depend on which variant you’re rendering.
The types for these things are horrifying.
I’m looking at a type definition in our codebase right now. I won’t paste the whole thing but here’s the shape of it. It’s a generic function with three type parameters, two conditional types, an intersection type, and a mapped type that uses template literal types to generate keys dynamically.
It works. It’s technically correct. And absolutely nobody on the team can read it without spending five minutes parsing the syntax.
That file has a comment at the top that says “sorry.” Put there by the person who wrote it. They knew.
We replaced JavaScript confusion with TypeScript confusion. The confusion just moved from runtime to compile time and got dressed up in angle brackets.
What TypeScript actually fixed
I want to be fair. I’m frustrated but I’m not delusional. TypeScript did fix some things.
Rename refactoring actually works now. In JavaScript, renaming a property across a large codebase was terrifying. You’d do a find-and-replace and pray. Now the compiler tells you everywhere you missed. That’s genuinely valuable. I’d guess it saves us a couple hours a month.
Autocompletion is much better. VS Code with TypeScript gives you real suggestions based on actual types instead of guessing. This helps. Especially for developers who are new to the codebase.
Certain categories of bugs did decrease. Specifically the “I passed a string where it expected a number” type of bug. Those are basically gone. That’s real.
But here’s my honest assessment of the cost-benefit after three years.
The time saved by catching type errors at compile time is real but smaller than I expected. Maybe five or six hours a month across the team.
The time spent on type system maintenance, fighting the compiler, writing complex generics, and onboarding new developers who know JavaScript but not TypeScript is much larger. I’d estimate 60 to 80 hours a month across the team.
Those numbers don’t work. Not even close.
The thing that annoys me most
What really bothers me isn’t the productivity cost. Productivity is hard to measure and reasonable people can disagree about the numbers.
What bothers me is the culture TypeScript created in the JavaScript community.
There’s this attitude now that if you’re writing plain JavaScript, you’re being irresponsible. That JavaScript is the “kiddie” version. That “real” engineers use TypeScript.
I’ve seen job postings that list TypeScript as a hard requirement for roles that are basically building CRUD apps. Forms and API calls. You do not need conditional mapped generic intersection types to build a contact form. But the listing says TypeScript required, so here we are.
I’ve watched junior developers spend weeks learning TypeScript generics before they understand how JavaScript promises work. They’re learning the type system before they understand the language it’s built on. That’s backwards.
And I’ve been in code reviews where someone rejected a perfectly working pull request because “the types could be stricter.” The code was correct. It was readable. It would have shipped and worked fine. But it got sent back for type improvements that no user would ever notice.
We’ve created a culture where type purity matters more than shipping software. And I don’t think that’s what anyone intended when TypeScript started.
What I think we got wrong
I’ve been thinking about this a lot. Not just complaining about it. Actually trying to understand where the disconnect happened.
I think the problem is that TypeScript is excellent for certain kinds of projects and we applied it to everything.
Library code? TypeScript makes sense. If you’re writing a package that thousands of developers will consume, strict types are a gift to your users. The investment pays off because the types serve as documentation for people who can’t read your source code.
Large teams with high turnover? TypeScript probably helps. When developers come and go frequently and nobody can hold the whole system in their head, types provide guardrails.
But a small team of five or six engineers working on a product? Where everyone knows the codebase? Where the main challenge is shipping features fast and iterating based on user feedback?
I think plain JavaScript with good tests is faster, simpler, and produces code that’s easier to change.
That’s not a popular opinion. I know. But I’ve now worked extensively with both. And when I’m honest with myself about where my time goes and what actually prevents production bugs, the answer is tests. Not types.
The bugs that hurt us. The ones that wake people up at 3 AM. Those are logic bugs. Business logic that doesn’t handle an edge case correctly. A race condition in an async flow. A database query that returns unexpected results under specific conditions.
TypeScript catches zero of those. Zero. Tests catch most of them.
Where I am now
We’re not migrating back to JavaScript. The cost of another migration would be worse than the cost of staying. That’s the honest reason we’re still on TypeScript. Not because it’s better. Because switching back would be even more disruptive than switching to it was.
That’s a terrible reason to stay with a technology. But it’s the real one.
What I’ve personally done is relax. I stopped trying to make every type perfect. I use any when fighting the compiler would take longer than the type safety is worth. I write simpler types even when a fancier generic would be “more correct.” I prioritize readable code over strictly typed code.
Some of my colleagues disagree with this approach. We’ve had arguments about it. One person on the team called my code “barely typed” which is technically an insult but honestly I took it as a compliment.
The code works. It ships. Users don’t care whether my function signature uses a generic conditional type or a simple union.
Users care whether the feature works.
If you’re considering the switch
I’m not going to tell you what to do. Every team and every codebase is different and anyone who gives universal advice about this is oversimplifying.
But I’ll tell you what I wish someone had told me three years ago.
The migration will take twice as long as you estimate. Budget accordingly. Don’t let anyone tell you it’s a “gradual” process. It’s gradual in theory. In practice it creates a two-language codebase that’s worse than either pure JavaScript or pure TypeScript.
The daily overhead is real and permanent. It’s not just a learning curve. Three years in, experienced TypeScript developers on my team still spend significant time on type system friction. It gets better. It never goes away.
The benefits are real but narrower than advertised. Renaming and autocompletion are genuinely better. Certain bug categories go away. But the big scary production bugs? Those are logic bugs. Types don’t help with logic.
Ask yourself honestly: what are the actual bugs your team deals with? If the answer is “we keep passing wrong types to functions” then TypeScript will help a lot. If the answer is “our business logic doesn’t handle these edge cases” then TypeScript won’t help at all and you’ve just added a tax to every line of code you write.
That’s the question nobody asks before adopting it. They probably should.
I’m curious where you land on this. If you’ve been through a TypeScript migration, did it play out the way you expected? And if you’re still on plain JavaScript, are you feeling pressure to switch? Drop it in the comments. This is one of those topics where I genuinely want to hear both sides, because I’m still figuring out my own answer.
Comments
Loading comments…