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.
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.
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?
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.
readonly keyword in TypeScript is nice, it makes properties immutable. However it is a long way from having support proper immutable data structures.
TypeScript & React — a match made in hell?
- 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:
 !=  . 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.
Too bad, TypeScript.
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
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.
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.
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.
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!
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.
What are your thoughts and experience? Have I missed anything important? Let me know in the comments.