Evojam

View Original

Static Typing in JavaScript & Best Tools to Do It

Let's multiply Object by a String and add an Array!

Said no sane developer ever.

Why not leave things as they are?

Once upon a time there was an App. It was made of common App building blocks. One of its important foundation stones was a UtilityBeltService (let's call it Utils). The Utils had so many methods that it was hard even for old master App builders to grab a proper one without a few minutes of reading its code. Many small type inconsistencies lurked in huge codebase. Disaster was only a matter of time…

One of the builders spent some time commenting all those methods with some odd annotations. But it only made Utils look ugly and did not help too much. The App said, it wanted to be clean and beautiful, to be developed agilely and smoothly.

The builders decided to send one of them to the dark wastelands of world wide web in search for The Solution. The chosen one spent several working hours in search of salvation and he brought back three magic books. They had to decide. And they decided. App was to be happy again and not only Utils would change…

The Solution: Type

What the builders thought they needed was static typing system for JavaScript.

To help them find bugs faster.

To make their code better.

Wait, but JS has types!

Yes. It has. But JS is dynamically typed. And it's dynamically typed with great elasticity - that's really useful in fast prototyping and simple one-job scripts. But when application grows this elasticity becomes real pain in the ass as type bugs come out in runtime or even after several months on production. And no, unit and e2e tests are not enough.

JavaScript uses coercion mechanism to deal with operations on vars of wrong types. For example "a" + 1 (which is adding String to Number) will go smoothly and return "a1" (as a String). Here Number 1 is implicitly converted to a String to fit the operation. And that's how coertion works. It smoothly hides many little and nasty bugs!

Static Typing

Consider "a" + 1 expression. What if some kind of checker would test it before running application? It would say Hey! I can Add Strings or Add Numbers but I do not know how add a String to a Number? Aaargh…. And sane developer can decide now what he really needs. Maybe he wants to getCharCode("a") + 1 or "a" + String(1)? The first one returns number 98, the second one string "a1".

I'm sure that you remember all those JS tweaks related to coertion (and not only this). But I'm also sure that everyone makes mistakes. And few such mistakes in a huge codebase can make one spend several hours in chase of The Bug.

I have good news. Static typing can find it for you even without running an application :)

Statically Typed JavaScript

Of course it can be only some kind of fake static typing applied on top of JS in one or other way. Not a real thing.

There are three popular possibilities - jsDoc (with e.g. Google Closure Compiler), Facebook Flow and Microsoft TypeScript. First one uses comment like annotations - does not corrupt pure JS - while next two extend JS so they have to be transpiled. I will compare them briefly in a moment.

Before I start: this article does not touch AtScript as it left the scene in favour of TypeScript. Also I do not want to talk here about completely different languages compiled to JS like Elm, Dart, ScalaJS or anything similar. It's just about static type system for JavaScript (and maybe little more).

JsDoc

JsDoc is annotation-like comment system that brings some standarisation to describing code in comments (based on JavaDoc). It's easy to start, as you do not have to change your builds, tests, etc. - you can just add few comments here, few there and you have an inline documentation. Then it can be easily compiled to human readable docs by jsDoc tools. The most interesting part, is that you can:

Then you can use proper tools to check code before a build. Or even use data type information about JavaScript variables to provide enhanced optimization with Google Closure Compiler. Nevertheless when using external libraries, you'll have to write definitions for them (see: @external). Also quite an issue is the fact that you have to add a lot of bloat to code - huge blocks of comments. It breaks the flow.

Flow

Facebook Flow is a static type checker, designed to find type errors in JavaScript programs. Actually it's a superset of JavaScript, which means that a valid JavaScript is also a valid Flow annotated JavaScript. But not the reverse. JS file, when annotated with flow, will not run in browser. But its file extension is still .js. May be quite misleading, right?

What really shines in Flow, is that its type inference is designed in a way that helps a developer to gradually evolve JavaScript code into typed code and to easily mix statically and dynamically typed code. It is so forgiving that it makes transition to Flow almost totally painless. See five simple examples of Flow in action.

It also supports ES6 classes and destructuring, generic types, polymorphic classes, not concrete definitions (interfaces), .jsx React files… and much more. All it makes Flow a mature static type checker. I'd go further then it's creators and call it a language.

TypeScript

Microsoft TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. As with both previous type checkers, you can start with JavaScript files that you already have. Just change their extension to .ts. I consider this a good idea, because TS code - same as with Flow - may not be a valid JS.

Converting bigger apps from JS may be a bit painful. TS uses implicit any type (just like Flow) but its type inference is different (more strict?):

Flow

TypeScript

  • type is "dynamically" changed on reassignment

  • less (or none) errors for reassigned variables

  • painless conversion

  • variable assignment sets its type forever

  • more type errors for reassigned variables

  • safety

TypeScript can add some pain. But this is the cost of strict typing. The cost of safety!

It also supports ES6 classes, destructuring, generators, await, async, modules, ES7 decorators and reflection and also generic types, polymorphic classes, interfaces, enums, abstract classes, .tsx that can be compiled to .jsx React files)… …all that Flow does and much more.

All of these makes TypeScript a mature, strictly statically typed… language. Yes, even if it's the superset of JavaScript, it actually became something more than a type checker. It's rather a language - still quite similar to JS - that transpiles to JavaScript.

One more great feature is DefinitelyTyped - the library of interfaces and definitions for commonly used libraries.

Comparison

JsDoc

Pros:

  • no need to compile/transpile to real JS

  • the fastest conversion

  • can be used for optimization (compressing with GCC)

Cons:

  • more a documenting tool than a type checker

  • adds a lot of ugly bloat to code

  • really painful to use with external libraries

Flow

Pros

  • fast and painless conversion from JS

  • mature type system

  • not nullable types

  • developed by conscious team and community

  • support for some ES6 features

  • usable with React's .jsx

Cons

  • forgiving type inference

  • same file extension as plain JS (chaos warning - .js --> .js)

  • painful use with external libraries see

  • is transpiled to JS

TypeScript

Pros

  • mature type system

  • strict type inference

  • most ES6 features

  • some ES7 features

  • different file extension (.ts --> .js)

  • DefinitelyTyped - easy use with most external libraries

  • possibly easy conversion from JS

  • support for React (.tsx --> .jsx)

Cons

  • is transpiled to JS

  • in huge, complicated codebase conversion may be painful

  • Microsoft (but it is only emotional)

Conclusion

It's all just a mistification. JavaScript is dynamically typed language. All compared type systems are only a layer applied on top of real code. But that's ok because IT WORKS :)

For our projects at Evojam we have chosen TypeScript. It works fabulously in our environment because we love type safety, good code quality, strict rules and the newest technologies (ES6, ES7).

Angular team's decision to use TypeScript had not influenced us as we started to use TS a month before they announced their move. jsDoc was not even considered - it's too ugly. Flow is less strict and have less features - it may be a good conversion route (to TypeScript) for large systems with a really huge codebase.

The nice part is that Flow and TypeScript teams seem to learn from each other. It can make both of them even better.

Further reading: