Ultraman Dyna, known for his Strong Type

On Hating the Obvious Choice

Or why I bet my future on TypeScript and hate myself for it

Wilfred Springer
Published in
11 min readJan 27, 2019

--

Somebody once defined intelligence to me as the ability to cope with conflicting views without breaking down. I hope I will be intelligent enough to cope with two conflicting views I hold. Views that undoubtedly will have a huge impact on my future: I don’t like TypeScript, but it will start using it nonetheless.

This is the post I just have to write to get past my disgust and to come to terms with my choice. It will be a long post. In fact, I hope it will be so exhaustive that I will never have to discuss the subject again. When people will ask me about my programming language preferences, I will simply point to this post.

Complaint 1: it’s C#/Java ported to the browser

I think the main reason for having TypeScript is to lower the bareer to entry for Java and C# programmers. However, let’s think about this for a while. Java dates back from 1996. It has been around for more than 20 years. Meanwhile, we learned a lot about programming languages. I feel those lessons learned have been mostly ignored, like for instance:

Complaint 2: statements? really?

I somehow figured that — by now — we would have learned that statements are increasing the chance of introducing errors. You need to declare variables as null or undefined outside a block, and then hope for the best that they’re getting assigned in one of the branches of the inside block. There’s always a chance that it won’t.

So, in Scala, and in all other programming languages that I care about (Nim, CoffeeScript, etc.), you can do this:

val a = if (true) 'foo' else 'bar'

But in TypeScript, you still need to do this:

let a: string;
if (true) {
a = 'foo';
} else {
a = 'bar';
}

Note that it in the Scala case, I’m using the keyword val to mark a to be immutable. In TypeScript, I just don’t have a way to do that. That is, I could have used the ternary if instead, but you cannot do the same with try or switch statements. As a consequence:

Complaint 3: it’s broken

What’s the use of a const keyword if there are quite some occasions where you cannot use it? If I would have had it my way, all variables would be defined as constants by default, offering the choice to explicitly mark something as mutable if you need it to be mutable. But in this case, TypeScript doesn’t really offer me the choice. And that, of course, is all because:

Complaint 4: TypeScript wants to be JavaScript with extras

TypeScript wants to be JavaScript, but then with extras. It doesn’t aim to fix the issues in JavaScript. All valid JavaScript should be considered valid TypeScript. As a consequence, fixing the issues in the language is a non-goal. It’s the opposite of what it aims to do:

Expression-level syntax is an explicit non-goal, although I’m sure many of the people involved in developing the language would consider it useful.
From Microsoft’s design goals document:

8. Avoid adding expression-level syntax.

… and from the non-goals:

1. Exactly mimic the design of existing languages. Instead, use the behavior of JavaScript and the intentions of program authors as a guide for what makes the most sense in the language.

What bugs me is that I haven’t been able to find a reason why they want to avoid expression-level syntax. I suspect it’s because it would require blocks to always evaluate to their final expression, which would set a whole bunch of other questions into motion, like, what to do with the return statement in a function.

Complaint 5: TypeScript inherits from JavaScript, the bad parts

Since it just wants to be JavaScript with extras, it also inherits the bad parts, sacrificing simplicity. In CoffeeScript, you don’t have to worry about using == or ===. In TypeScript, you have three different ways to do multiline Strings, just because JavaScript already has it.

Complaint 6: TypeScript will never try to outpace JavaScript

As a consequence, TypeScript will not get an Elvis operator before JavaScript gets it. And that’s horrible, because:

Complaint 7: I miss the Elvis operator

In JavaScript, references can be undefined or null. As a consequence, invoking a method on such a reference will get you in trouble. CoffeeScript already has the Elvis operator. In the snippet below, if the reference to person is null or undefined, then it simply will not invoke greet() and the entire expression will result in undefined.

person?.greet()

The JavaScript and TypeScript alternative is just awful.

!!person ? person.greet() : void 0

Now, I don’t have a problem with treating optionals a little bit more explicit. However, in that case, I would have preferred the language to either just have better support for optional types, like in Scala. (As an aside: the fact that this works pretty smooth in Scala has to do with how it factors for expressions in invocations of map, flatMap and filter. The Option[T] type just happens to have these operations.)

val person: Option[Person];
greeting = for (p <- person) yield person.greet();
# Resulting in None if person is None or a Some if person is
# Some[Person]

Complaint 8: TypeScript overemphasizes a class based type system

I said it before, and I will say it again, and I will say more about it in a moment: I don’t have a problem with strongly typed languages. I ❤️ ReasonML and Nim. But feeling love for strongly typed languages doesn’t necessarily imply feeling love for classes, in the way Java and C# define them.

So, a couple of words on that. Encapsulation is fine. Polymorphism is fine. I can live without inheritance. Unfortunately, TypeScript opens the door for Java and C# developers pouring in and creating deep subclassing hierarchies.

What I also spotted — which makes perfect sense if you’re coming in from a world without first-class functions— is wrapping every simple operation in a class, which offers you another way to pass behavior around. But TypeScript and JavaScript already have a way to pass behavior or strategies around, by simply passing functions around.

To be fair, the fact that there is often a simple way to do things in cases where Java and C# would define a class is not an issue of TypeScript itself. It’s a problem of the limited exposure of Java and C# developers to something else. But TypeScript does make it very easy to just keep coding in exactly the same way.

Now, this again is a situation where TypeScript just blindly inherits whatever ES6 has. But I just wish they hadn’t. If you do want to have a class concept, then —gosh— I just wish they had adopted Scala’s version instead. It would have been so much more compact, and it wouldn’t have inherited static members.

I reserve some special agony for static methods. Static methods and properties are just awful. If you’re coming from Java or C#, you have the impression you need them. But if you’re coming from Scala, then it becomes obvious that you don’t, and that having a companion object is by far the better option. I won’t explain it all here, but just ask yourself how you would make your static properties and methods implement an interface. (Answer: you can’t.)

The last reason why I am personally not a huge fan of class-based types is that they hardly ever help you to model a business concept, however, that’s the thing people will start using it for, inevitably. First of all, real world objects hardly ever fit the rigor of something with a predefined static list of properties. Libraries like Joi are — I think— better suited to capture the dynamic of real world objects. But since classes are getting more emphasis now, people coming from Java and C# will do what they have always done: consider classes as the best solution for capturing their domain model.

(The ultimate way to capture your domain logic is obviously event sourcing, and classes are not going to be helpful to model the structure of your aggregate root in different states.)

Complaint 9: TLDs can lie — and they do

People love the fact that TypeScript provides compile time proof of what you’re doing. Unfortunately, some of that validation is really based on what library authors have provided as type descriptors. So even though it suggests type safety, it only provides safety as long as the type definitions don’t lie, which is not guaranteed if the library itself is written in something else than TypeScript, which brings me to:

Complaint 10: TypeScript is viral

I see many library authors who had their libraries written in JavaScript or CoffeeScript before, now rewriting it into TypeScript, mainly to make sure they’re lowering the bareer to use their libraries in TypeScript projects.

Complaint 11: Once it’s in, it’s just one step away from your users being dependent on it

Case in point: Angular. I don’t think anybody in their sane mind would consider using Angular without using @Component decorators. As a consequence, using Angular is just a horrible experience in any other language than TypeScript.

Complaint 12: Therefore, it reminds me of J++

I realize that people who are younger than I am have no idea what I’m talking about. So let me give you a little history lesson:

Java was launched in 1996, by Sun Microsystems. When other vendors started to get behind Java (IBM, and others), Microsoft also jumped the bandwagon, and launched J++ in 1997. J++ was able to run vanilla Java byte code, but it also introduced some additional byte code, causing J++ byte code to be incompatible with other JVMs.

This was obviously troublesome. It broke the ‘write once, run anywhere’ promise of Java. It resulted in a lawsuit, in which Microsoft eventually settled by paying Sun a handsome 1.6 billion dollar.

Somewhere, in the back of my mind, it smells similar. It’s smells like a way to lure all developers back into Microsoft’s world, and that we all realize it only after it’s too late.

So, why am I going to use it nonetheless?

That’s the thing I wondered about myself as well. I can give you a couple of reasons:

Benefit 1: structural typing, union types, etc.

I don’t like classes. But I do like what they did to support structural typing. Admittedly, I’d prefer ReasonML’s version, but this could be good.

Benefit 2: it generates vanilla JavaScript

CoffeeScript generates JavaScript that seems like it was written by hand. I wish I could say the same for ReasonML, but I can’t. I can’t think of a real world ReasonML program that would not immediately start importing a bunch of ReasonML specific runtime dependencies. And there is something that bothers me about that.

Benefit 3: it works with Vue

I like the simplicity and coherence of Vue. I love that it allows me to use alternative implementations for anything inside components. Stylus instead of vanilla CSS, CoffeeScript instead of vanilla JavaScript and Pug instead of vanilla HTML. It also supports TypeScript. I don’t see too many serious attempts to have support for ReasonML as well.

Benefit 4: massive adoption

Perhaps not a benefit. But there is benefit in having widespread adoption.

Benefit 5: code completion

If going from Scala to Node.js has taught me one thing, then it must be that I can write code without code completion support in tools. I can. I don’t think I will be dependent on it. But it’s a nice-to-have feature.

Other Benefits?

I currently cannot think of any. There might be something out there that I’m completely missing. Also, I’m not changing to TypeScript because I love it. I don’t even think it’s a better language than CoffeeScript. In fact, I still think that CoffeeScript is a better language than TypeScript: it’s simpler, everything is an expression, it has the Elvis operator, you can always use the compact notation for a function, it’s story on string literals and interpolation is a lot more concise than the ES6 legacy in TypeScript, it allows me to treat things as constants even if it doesn’t have a keyword for it.

The Missing Benefit

You might have noticed that I’m not explicitly listing compile time safety here. The reason is that I first of all seem to be able to do without. The other reason is that TypeScript compilation is slow, and that’s obviously to blame to all the type-checking. And if there’s one thing that I have learned is that a fast feedback loop and fast executing tests beat compiler checks. The last reason why I haven’t listed compile time safety here is that I keep running into TypeScript codebases where all compile-time safety is annihilated by forcing things to be of type any and then explicitly casting values to the type the programmer expected it to be — which is obviously not the best way to benefit from the compiler.

What about the alternatives?

Throughout this article, I have mentioned some alternatives. This article is already too long as it is now, but let me briefly list some of the reasons why I’m not placing my bet on the other languages:

  1. ReasonML: ReasonML is the language that makes me hesitate about all of the above. There’s a lot in there that I like: pattern matching, immutability, expression based syntax, the compilation speed, etc. However, it seems incomplete (escaping to BuckleScript and JavaScript), some things are a little awkward (implicit conversions, operator overload) and it also forces a whole bunch of other choices upon me, like a particular web framework. I do have some fear that React’s not-invented-here syndrome is spilling over into ReasonML as well though.
  2. Elm: Again, there’s a lot that I like, but I love being able to write in the same language for both server-side and client-side code.
  3. PureScript: I have never given it a real go. Perhaps I should. I just get the impression that it runs the risk of having the community getting invaded by FP fundamentalism.
  4. CoffeeScript: Admittedly, it started loosing traction when JavaScript started importing some of its features, and TypeScript came alongside. It doesn’t have offer any compile-time validation. It has many people complaining about its syntax. But to me, it’s the RISC version of its CISC brother JavaScript. I can’t think of any other language that allows me to write code that is so compact and concise.

How to move forward

As I said, I’m betting my money on TypeScript for now. However, it’s a marriage of convenience. As a consequence, I will keep my eyes open for an alternative, that will have to come some day. Something that is more functional, but keeps the fundamentalists away. Something that is not afraid to break with JavaScript. Something that supports a more compact notation. Something that may be typesafe, but without getting in the way. Something that has expression syntax only. Something that returns from a compound expression with the value of the last expression. Something that promotes immutability in a sane way.

Meanwhile, I will keep investing some time in ReasonML too. Just because of its soothing effect. And whenever I need to get something done quickly, I’ll return to CoffeeScript.

A last word on ReasonML. One of the things that is nice about it is that it compiles to OCaml and therefore could be considered a systems language as well. I will look into that a little more. I have been waiting for a systems language that is not Rust for a while. ReasonML might be it. But I wonder. I have been reading up on Nim lately. That might be the better choice.

--

--

Wilfred Springer
East Pole

Double bass playing father of three, hacker and soul searcher