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:
define type of variable
define type of function argument
define type of function return
or even define new type
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:
some info on JavaScript’s Type System
real life use case: TypeScript vs Flow - investigation
another sane comparison: Statically typed JavaScript