Problems With TypeScript in 2020
Posted on
TypeScript is very good. We ported our frontend and backend to it with no regrets. We have a much smaller test suite than we ever could've had in a dynamic language. All of this is great. Still, there are downsides to TypeScript, and we should be honest about them too.
JavaScript
TypeScript is a superset of JavaScript, and JavaScript is a mess. It was designed in 10 days, then it evolved haphazardly for almost two decades, then it was finally cleaned up in the last few years. A lot of the warts are still there and will always be there. For example:
1 * '1'returns1instead of an error.({name: 'Amir'}).agereturnsundefinedrather than erroring.x && x + 1looks like it means "add 1 to x if it's not null or undefined"; but when x is 0 it returns 0, not 1.
TypeScript can't fix all of JavaScript's warts. But having used JavaScript since the 90s, I'm amazed at how well TypeScript did its dirty, improbable job. The first two code examples in the previous list are illegal in TypeScript. By adding a linter, the third one becomes illegal too. Those examples are simple, but TypeScript has saved us from many complex, subtle bugs that are too complex to show here.
The TypeScript team has been working to standardize new JavaScript features that let them increase safety further. For example, the new nullish coalescing operator mitigates JavaScript's design defects around the falsiness of 0 and the empty string, "".
Overall, the JavaScript underneath TypeScript is still frustrating in many situations, but TypeScript mitigates many of the worst frustrations. (A past version of myself would be very surprised to see this future!)
Compiler bugs
For decades, programmers joked that "It's never a compiler bug." Compilers weren't perfect, but "compiler bugs" almost always turned out to be the programmer misunderstanding the language.
When used as a long-lived watcher process, TypeScript has broken this trend: it's buggy. The compiler tends to hit bugs when files are deleted or renamed. We have a special script that notices those events and restarts the compiler. But our script is imperfect, so we still end up restarting the compiler manually. Last Saturday, I manually restarted the TypeScript compiler dozens of times as it encountered multiple bugs over and over again.
Sometimes the compiler doesn't realize that a file was deleted, so it produces incorrect errors. For example, if file A imports file B, and we delete both, it will sometimes complain that file A is trying to import B, which doesn't exist. But A doesn't exist either, so it shouldn't be causing an error! Other times, the compiler "successfully" compiles when there are actually type errors. In all of those cases, killing and restarting the compiler makes the errors go away, which is an unambiguous sign that it's a compiler bug in the watcher's state management.
(Webpack's watcher also has similar bugs around file deletion and renames. The easiest workaround for both TypeScript and Webpack is to restart all of the tools when a file is renamed or deleted, then wait as they all start back up. I feel embarrassed while waiting for those restarts.)
In the compiler's defense, this all seems to be limited to the watcher. I've never seen a bug in the compiler when it's cold booted, as it is during a production deploy or a CI run. And TypeScript doesn't seem worse than most other long-running development tools. As any long-time IDE user knows, any development tool that runs for a long time will break.
Unfortunately, booting the TypeScript compiler is very expensive, so we have no choice but to keep the compiler running between changes. This is in contrast to some other static languages like Go and Reason, which can compile systems the size of Execute Program in the neighborhood of 100 times faster than TypeScript. Those compilers are fast enough to be booted as-needed and then terminated. No need for a long-lived watcher.
The Node/NPM ecosystem
TypeScript is a superset of JavaScript, so it's closely connected to the Node and NPM ecosystems. Like any large ecosystem, the Node and NPM ecosystem is a mixed bag. There are so many packages; it's great! But some of them are very good and others are less good.
TypeScript allows us to write types for existing JavaScript libraries. People (often not a package's original authors) publish these type definitions in a central repository. On GitHub, that repository says "The repository for high-quality TypeScript type definitions." There's a bit of aspiration in that description. Sometimes it's true and the type definitions are high-quality! Sometimes they're not.
As one example, we recently did a major version upgrade of the database library that underpins our own database library. That made some of our valid code fail to type check. Worse, it allowed other code to type check even though it had major type errors. From our perspective, the "upgrade" was a step backward. But we don't have a choice if we want to continue to get security updates, and continue to upgrade other packages that depend on the database library.
None of that happened because TypeScript is bad; it happened because the type definitions for that library are wrong. Fortunately, our recent experience with that database library is an outlier. Most of the time, everything goes fine. Still, this is an annoyance that's relatively unique to TypeScript because it's a type system layered over an existing dynamic language.
It's hard to say whether the Node+NPM+TypeScript ecosystem is "better" or "worse" than, say, Ruby's or Python's. However, it's definitely far more complex, and it requires a more careful approach. Sometimes, there will be 50 packages that claim to do the thing that we need. Sometimes, one of them has more downloads than the other 49 combined. Sometimes, the best solution is the ninth-most-popular.
Overall, less-experienced programmers face an uphill battle in deciding which packages are worth using. Adding TypeScript increases that burden: now we have to analyze all of the library options, and we also have to analyze the presence and quality of third-party type definitions. Arguments for or against packages may be written by JavaScript users, so they won't be considering the quality of the library's TypeScript type definitions.
These problems don't outweigh the benefits. Compiler bugs are annoyances, but they've never affected our CI or production environments. Ecosystem complexity is a danger that can be navigated with forethought and by learning from past mistakes. I mention these problems to balance out the praise in earlier articles about TypeScript, but for us the trade-off is unambiguously in TypeScript's favor!
Gary Bernhardt