This article takes a more critical, data-driven approach to analyze the ROI of using TypeScript to build large scale applications.
But if you’re in the position of deciding whether or not to use it, you should have a realistic understanding of both the benefits and the costs. Will it have a positive or negative impact?
In my experience, it has both, but falls short of positive ROI. Many developers love using it, and there are many aspects of the TypeScript developer experience I genuinely love. But all of this comes with a cost.
My understanding of TypeScript, including its benefits, costs, and weaknesses have deepened considerably. I’m saddened to say that it wasn’t as successful as I’d hoped. Unless it improves considerably, I would not pick TypeScript for another large scale project.
I’m still long-term optimistic about TypeScript. I want to love TypeScript, and there’s a lot I still do love about it. I hope that the TypeScript developers and proponents will read this as a constructive critique rather than a hostile take-down piece. TypeScript developers can fix some of the issues, and if they do, I may repeat the ROI analysis and come to different results.
Static types can be very useful to help document functions, clarify usage, and reduce cognitive overhead. For example, I usually find Haskell’s types to be helpful, low-cost, pain-free, and unobtrusive, but sometimes even Haskell’s flexible higher-kinded type system gets in the way. Try typing a transducer in Haskell (or TypeScript). It’s not easy, and probably a bit worse than the untyped equivalent.
I love that type annotations can be optional in TypeScript when they get in the way, and I love that TypeScript uses structural typing and has some support for type inference (though there’s a lot of room for improvement with inference).
I’m going to rate TypeScript on several dimensions on a scale of
-10–10 to give you a better sense of how well suited TypeScript may or may not be for large scale applications.
Greater than 0 represents a positive impact. Less than 0 represents a negative impact. 3–5 points represent relatively strong impact. 2 points represents a moderate impact. 1 point represents a relatively low impact.
These numbers are hard to measure precisely, and will be somewhat subjective, but I’ve estimated the best I can to reflect the actual costs and rewards we saw on real projects.
With margin-of-error so broad, I gave up on objective quantification, and instead focused on feature delivery pace and observations of where we spent our time. You’ll see more of those details in the ROI point-by-point breakdown.
Because there’s a lot of subjectivity involved, you should allow for a margin of error in interpretation (pictured in the chart), but the over-all ROI balance should give you a good idea of what to expect.
I can already hear the peanut gallery objections to the small benefits scores, and I don’t entirely disagree with the arguments. TypeScript does provide some very useful, powerful capabilities. There’s no question about that.
Let’s look at each point in more detail.
In fairness, if you use default parameters to provide type hints, you don’t need to supply the annotations for TypeScript code, either, which is a great trick to reduce type syntax overhead — one of the overhead costs of using TypeScript.
TypeScript’s tooling for these things is arguably a little better, and more all-in-one — but it’s not enough of an improvement to justify the costs.
Even with the best inline documentation in the world, you still need real documentation, so TypeScript enhances, rather than replaces existing documentation options.
Type safety doesn’t seem to make a big difference. TypeScript proponents frequently talk about the benefits of type safety, but there is little evidence that type safety makes a big difference in production bug density. This is important because code review and TDD make a very big difference (40% — 80% for TDD alone). Pair TDD with design review, spec review, and code review, and you’re looking at 90%+ reductions in bug density. Many of those processes (particularly TDD) are capable of catching the same class of bugs that TypeScript catches, as well as many bugs that TypeScript will never be able to catch.
TypeScript is only capable of addressing a theoretical maximum of 15% of “public bugs”, where public means that the bugs survived past the implementation phase and got committed to the public repository, according to Zheng Gao and Earl T. Barr from University College London, and Christian Bird from Microsoft Research.
Their study looked at bugs that were known in advance, including the exact lines that were changed to fix the bugs in question, where the problem and potential solutions were known prior to introduction of typings. What this means is that even knowing that the bugs existed in advance, TypeScript was unable to detect 85% of public bugs.
Why are so many bugs undetectable by TypeScript, and why did I call that 15% reduction a “theoretical maximum” effect? For starters, specification errors caused about 78% of the publicly classified bugs studied on GitHub. The failure to correctly specify behaviors or correctly implement a specification is the most common type of bug by a huge margin, and that fact automatically renders an overwhelming majority of bugs impossible for TypeScript to detect or prevent. In “To Type or Not to Type”, the study authors identified and classified a range of “ts-undetectable” bugs.
“StringError” above are the classification of errors where the string was the right type, but contained the wrong value (like an incorrect URL). There is some limited potential for static analysis to make a small dent in StringErrors by gaining some visibility into the values of strings, as dependent type systems are capable of, but that potential is a small percent of a small percent, so there’s little potential that TypeScript will ever be capable of detecting more than 15%-18% of bugs.
But a 15% sounds like a lot! Why doesn’t TypeScript get much higher bug prevention points?
Because there are so many bugs that are not detectable by static types, it would be irresponsible to skip other quality control measures like design review, spec review, code review, and TDD. So it’s not fair to assume that TypeScript will be the only thing you’re employing to prevent bugs. In order to really get a sense of ROI, we have to apply the bug reduction math after discounting the bugs caught by other measures.
Imagine your project would have contained 1,000 bugs with no bug prevention measures. After applying other quality measures, the potential production bug count is reduced to 100. Now we can look at how many additional bugs TypeScript would have prevented to get a truer sense of the bug catching return on our TypeScript investment. Since close to 80% of bugs are not detectable by TypeScript, and all TypeScript-detectable bugs can potentially be caught with other measures like TDD, we’ll use a generous 8% further reduction (assuming about half of ts-detectable bugs will be missed by our other measures).
- No measures: 1000 bugs
- After other measures: 100 bugs remain — 900 bugs caught
- After adding TypeScript to other measures: 92 bugs remain — 8 more bugs caught
Some people argue that if you have static types, you don’t need to worry about writing so many tests. Those people are making a silly argument. There is really no contest. Even if you’re going to employ TypeScript, you still need the other measures.
In this scenario, TypeScript catches 150/1,000 bugs by itself, while other measures (not including TypeScript) catch 900/1,000 bugs. You obviously don’t have to pick one or the other, you can combine both and catch about 908 bugs out of 1,000 (remembering that the other measures will catch lots of the bugs you would have caught with TypeScript alone).
Having implemented quality control systems on large scale, multi-million dollar development projects, I can tell you that my expectations for effectiveness on costly system implementations are in the territory of 30% — 80% reductions. You can get those kinds of numbers from any of the following:
- Design and Spec Review (up to 80% reduction)
- TDD (40% — 80% reduction of remaining bugs)
- Code Review (an hour of code review saves 33 hours maintenance)
It turns out that type errors are just a small subset of the full range of possible bugs, and there are other ways to catch type errors. The data is in, and the result is very clear: TypeScript won’t save you from bugs. At best, you’ll get a very modest reduction, and you still need all your other quality measures.
Type correctness does not guarantee program correctness.
It looks like neither developer tooling nor type safety are really living up to the TypeScript hype. But those can’t be the only benefits, right?
To figure that out, we need to take a closer look at the costs of TypeScript.
On the other hand, if you only need to hire one or two developers, using TypeScript may make your opening more attractive to almost half the candidate pool. For small projects, it may be a wash, or even slightly positive. For teams of hundreds or thousands, it’s going to swing into the negative side of the ROI error margin.
TypeScript could improve on this cost by providing better documentation and discovery of TypeScript’s current limitations, so developers waste less time trying to get it to behave well on higher order functions, declarative function compositions, transducers, and so on. In many cases, a well-behaved, readable, maintainable TypeScript typing simply isn’t going to happen. Developers need to be able to discover that quickly so that they can spend their time on more productive things.
Ongoing Mentorship: While people get productive with TypeScript pretty quickly, it does take quite a bit longer to get feeling confident. I still feel like there’s a lot more to learn. In TypeScript, there are different ways to type the same things, and figuring out the advantages and disadvantages of each, teasing out best practices, etc. takes quite a bit longer than the initial learning curve.
For example, new TypeScript developers tend to over-use annotations and inline typings, while more experienced TypeScript developers have learned to reuse interfaces and create separate typings to reduce the syntax clutter of inline annotations. More experienced developers will also spot ways to tighten up the typings to produce better errors at compile time.
This extra attention to typings is an ongoing cost you’ll see every time you onboard new developers, but also as your experienced TypeScript developers learn and share new tricks with the rest of the team. This kind of ongoing mentorship is just a normal side-effect of collaboration, and it’s a healthy habit that saves money in the long term when applied to other things, but it comes at a cost, and TypeScript adds significantly to it.
Typing Overhead: In the cost of typing overhead, I’m including all the extra time spent typing, testing, debugging, and maintaining type annotations. Debugging types is a cost that is often overlooked. Type annotations come with their own class of bugs. Typings that are too strict, too relaxed, or just wrong.
This cost center has gone down since I first explored it, because many third party libraries now contain typings, so you don’t have to do so much work trying to track them down or create them yourself. However, many of those typings are still broken and out-of-date in all but the most popular OSS packages, so you’ll still end up backfilling typings for third party libraries that you want type hints for. Often, developers try to get those typings added upstream, with widely varied results.
You may also notice greatly increased syntax noise. In languages like Haskell, typings are generally short one-liners listed above the function being defined. In TypeScript, particularly for generic functions, they’re often intrusive and defined inline by default.
Instead of adding to the readability of a function signature, TypeScript typings can often make them harder to read and understand. This is one reason experienced TypeScript developers tend to use more reusable typings and interfaces, and declare typings separately from function implementations. Large TypeScript projects tend to develop their own libraries of reusable typings that can be imported and used anywhere in the project, and maintenance of those libraries can become an extra — but worthwhile — chore.
Syntax noise is problematic for several reasons. You want to keep your code free of clutter for the same reasons you want to keep your house free of clutter:
- More clutter = more places for bugs to hide = more bugs.
- More clutter makes it harder to find the information you’re looking for.
Clutter is like static on a poorly tuned radio — more noise than signal. When you eliminate the noise, you can hear the signal better. Reducing syntax noise is like tuning the radio to the proper frequency: The meaning comes through more easily.
Syntax noise is one of the heavier costs of TypeScript, and it could be improved on in a couple ways:
- Better support for generics using higher-kinded types, which can eliminate some of the template syntax noise. (See Haskell’s type system for reference).
- Encourage separate, rather than inline typings, by default. If it became a best practice to avoid inline typings, the typing syntax would be isolated from the function implementation, which would make it easier to read both the type signature and the implementation, because they wouldn’t be competing with each other. This could be implemented as a documentation overhaul, along with some evangelism on Stack Overflow.
I still love a lot of things about TypeScript, and I’m still hopeful that it improves. Some of these cost concerns may be adequately addressed in the future by adding new features and improving documentation.
However, we shouldn’t brush these problems under the rug, and it’s irresponsible for developers to overstate the benefits of TypeScript without addressing the costs.
TypeScript can and should get better at type inference, higher order functions, and generics. The TypeScript team also has a huge opportunity to improve documentation, including tutorials, videos, best practices, and an easy-to-find rundown of TypeScript’s limitations, which will help TypeScript developers save a lot of time and significantly reduce the costs of using TypeScript.
I’m hopeful that as TypeScript continues to grow, more of its users will get past the honeymoon phase and realize its costs and current limitations. With more users, more great minds can focus on solutions.
As TypeScript stands, I would definitely use it again in small open-source libraries, primarily to make life easier for other TypeScript users. But I will not use the current version of TypeScript in my next large scale application, because the larger the project is, the more the costs of using TypeScript compound.