TypeScript Will Make You Suffer.

Elliot Suzdalnitski
The Startup
Published in
10 min readDec 5, 2020

Have you fallen into the TypeScript hype? You definitely are not alone. TypeScript’s benefits over JavaScript may be questionable.

Let’s find out why.

Type systems are overrated

Many people swear by type systems. I tend to agree, type systems eliminate a large number of errors in programs, and make refactoring easier. Yet “having” a type system is only one part of the story. There are things that matter far more than static typing, and presence/lack of a type system shouldn’t be the only factor when choosing a language.

Throughout my career, I’ve used a large number of programming languages. And I can tell you, the TypeScript type system is pretty rudimentary, especially in comparison to other modern languages (think Rust, Scala, Haskell, OCaml, and plenty of others).

If a language has a type system, then it is also very useful to have type inference. The best type systems are able to infer most of the types, without annotating function signatures explicitly. Unfortunately, type inference provided by TypeScript is rudimentary.

Let’s find out if TypeScript really provides anything of value besides a rudimentary type system.

Nulls?

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

- Tony Hoare, the inventor of Null References

Why are null references bad? Null references break type systems. When null is the default value, we can no longer rely on the compiler to check the validity of the code. Any nullable value is a bomb waiting to explode. What if we attempt to use the value that we didn’t think might be null, but it in fact is null? We get a runtime exception.

We have to rely on manual runtime checks to make sure that the value we’re dealing with isn’t null. Even in a statically-typed language like TypeScript, null references take away many benefits of a type system.

Such runtime checks (sometimes called null guards) in reality are workarounds around bad language design. They litter our code with boilerplate. And worst of all, there are no guarantees that we won’t forget to check for null.

In a good language, the lack or presence of a value should be type-checked at compile-time. This is typically done using the Option pattern. Here’s an example in ReasonML:

What about TypeScript? TypeScript 2.0 has added support for non-nullable types, it can optionally be enabled using the--strictNullChecks compiler flag. But. Programming with non-nullable types is not the default, and is not considered to be idiomatic in TypeScript.

Too bad, TypeScript.

Immutability?

I think that large objected-oriented programs struggle with increasing complexity as you build this large object graph of mutable objects. You know, trying to understand and keep in your mind what will happen when you call a method and what will the side effects be.

Rich Hickey, creator of Clojure.

Programming with immutable values nowadays is becoming more and more popular. Even modern UI libraries like React are intended to be used with immutable values. Immutability definitely eliminates a whole category of bugs from our code.

What is immutable state? Simply put, it is data that doesn’t change. Just like strings in most programming languages. For example, capitalizing a string will never change the original string — a new string will always be returned instead.

Immutability takes this idea further, and makes sure that nothing is ever changed. A new array will always be returned instead of changing the original one. Updating user’s name? A new user object will be returned with its name updated, while leaving the original one intact.

With immutable state, nothing is shared, therefore we no longer have to worry about the complexity of thread safety. Immutability makes our code easy to parallelize.

Functions that do not mutate(change) any state are called pure, and are significantly easier to test, and to reason about. When working with pure functions, we never have to worry about anything outside of the function. Simply focus on just this one function that you’re working with, while forgetting about everything else. You can probably imagine how much easier development becomes (in comparison to OOP, where an entire graph of objects has to be kept in mind).

Immutability in TypeScript?

Dealing with immutable data structures in TypeScript is significantly worse than in JavaScript. While JavaScript developers can use libraries that help with immutability, TypeScript developers typically have to rely on the native array/object spread operators (copy-on-write):

Unfortunately, the native spread operator doesn’t perform a deep copy, and manually spreading deep objects is cumbersome. Copying large arrays/objects is also not good for performance.

The readonly keyword in TypeScript is nice, it makes properties immutable. However it is a long way from having support proper immutable data structures.

JavaScript has good libraries for working with immutable data (like Rambda/Immutable.js). However, getting such libraries to work with the TypeScript type system can be very tricky.

JavaScript is a clear winner for immutability. Too bad, TypeScript.

TypeScript & React — a match made in hell?

Dealing with immutable data in JavaScript [and TypeScript] is more difficult than in languages designed for it, like Clojure.

- Straight from React Documentation

Continuing from the previous drawback, if you’re doing frontend web development, then the chances are that you’re using React.

React was not made for TypeScript. React initially was made for a functional language (more on this later). There’s a conflict between programming paradigms — TypeScript is imperative, while React is functional.

React expects its props to be immutable, while TypeScript has no proper built-in support for immutable data structures.

What about performance? If you’re not careful, subtle performance issues can be introduced:

Such innocent-looking code can become a performance nightmare, since in JavaScript [] != [] . The above code will cause the HugeList to re-render on every single update, even though the options value hasn’t changed. Such issues can compound, until the UI eventually becomes impossible to use.

The only real benefit that TypeScript provides over JavaScript for React development is not having to worry about PropTypes. This benefit is really questionable, considering the other drawbacks.

Too bad, TypeScript.

Superset of JavaScript

Yes, being a superset of JavaScript has helped a lot with the adoption of TypeScript. After all, a lot of people already know JavaScript.

However, being a superset of JavaScript is more of a drawback. This means that TypeScript carries all of the JavaScript baggage. It is limited by all the bad design decisions made in JavaScript.

For example, how many of you like the this keyword? Probably nobody, yet TypeScript has deliberately decided to keep that in.

How about the type system acting really weird at times?

[] == ![];    // -> true
NaN === NaN; // -> false

In other words, TypeScript shares all of the drawbacks with JavaScript. Being a superset of a bad language can’t turn out to be good.

Too bad, TypeScript.

Algebraic Data Types?

A good type system should support Algebraic Data Types. ADTs are a powerful way of modeling application state. One can think of them as Enums on steroids. We specify the possible “subtypes” that our type can be composed of, along with its constructor parameters:

Yes, one can attempt to make use of Algebraic Data Types in TypeScript (Discriminated Unions):

Let’s take a look at the same piece of code implemented in ReasonML:

The TypeScript syntax is not as good as in functional languages. Discriminated Unions were added in TypeScript 2.0 as an afterthought. In the switch, we’re matching on strings which is error-prone, and the compiler won’t warn us if we miss a case.

Too bad, TypeScript.

Pattern matching?

A modern language should have good support for pattern matching. In general, pattern matching allows one to write very expressive code.

Here’s an example of pattern matching on an option(bool) type in a functional language:

Same code, without pattern matching:

No doubt, the pattern matching version is much more expressive and clean. Yet, this is not possible with TypeScript, since it does not provide pattern matching capabilities in a switch statement.

Proper pattern matching also provides compile-time exhaustiveness guarantees, meaning that we won’t accidentally forget to check for a possible case. No such guarantees are given in TypeScript.

Too bad, TypeScript.

Error handling?

Catching exceptions is a bad way to handle errors. Throwing exceptions is fine, but only in exceptional circumstances, when the program has no way to recover, and has to crash. Just like nulls, exceptions break the type system.

When exceptions are used as a primary way of error handling, it is impossible to know whether a function will return an expected value or blow up. Functions throwing exceptions are also impossible to compose.

Obviously, it is not ok for an entire application to crash simply because we couldn’t fetch some data. Yet this is what really happens more often than we’d like to admit.

One option is to manually check for raised exceptions, but this approach is fragile (we may forget to check for an exception), and adds a lot of noise:

Nowadays there are much better mechanisms of error handling, possible errors should be type-checked at compile-time. Here’s an example in Rust:

Modern languages like ReasonML, F#, Go and Rust use better alternatives to error handling, yet TypeScript designers has decided that exceptions by default is good enough. Too bad, TypeScript.

Verdict

The benefits that TypeScript provides over JavaScript are overrated. TypeScript has really failed to deliver by keeping all of the bad parts of JavaScript, effectively inheriting decades of bad design decisions made in JavaScript.

The problem with TypeScript is not what it has, but what it doesn’t have. TypeScript focuses too much on types, while missing many crucial features available in other modern languages. In most cases, you’re better off using good old JavaScript, especially for frontend development with React.

Is TypeScript just a hype? That’s up to you to decide. I think it is. Why is HypeScript so popular then? The same reason that Java and C# became popular for — being backed by multi-billion corporations with huge marketing budgets.

TypeScript isn’t any good, now what?

Good question, I’m glad that you asked!

Photo by Michael Dziedzic on Unsplash

Let me introduce you to a great modern language, that is not a superset of good old JavaScript. Just like TypeScript, this language is statically typed. It is being developed by Facebook, and made all the right decisions. It also builds upon a mature 20-year-old programming language.

This language compiles to JavaScript, and therefore has access to the entire JavaScript ecosystem. Its type system is amazing, having great support for Algebraic Data Types. Its compiler is able to infer almost everything.

The language itself has fever features than JavaScript, and in fact is much simpler than JavaScript.

This language has built-in support for immutable data structures, and it has no null references.

It also is a great fit for React. In fact, the creator of React himself is working on this language. It is statically-typed (just like TypeScript), and there’s no need to worry about PropTypes.

Remember the innocent-looking example that can cause performance disasters?

This awesome language has proper support for immutable data structures, and such code will not create performance issues:

Unlike with TypeScript, nothing gets unnecessarily re-rendered, great React performance out-of-the-box!

What is this wonderful language? You may have guessed, it is ReasonML. There’s no better option for frontend web development. And I’m ready to bet that ReasonML is the future of frontend web development.

Conclusion

ReasonML probably is what TypeScript has always aimed to be, but failed. ReasonML adds static typing to JavaScript, while removing all of the bad features (and adding in modern features that truly matter).

TypeScript = JavaScript + Types
ReasonML = JavaScript + Types - Bad + Good

What are your thoughts and experience? Have I missed anything important? Let me know in the comments.

--

--