One of the things that I found difficult when writing and refactoring in Ponos was maintaining the borders between the few classes and their functions. Some of the class methods returned values, some could throw errors, and others returned Promises. Relying on the inline documentation was insufficient because methods were changing faster than I was updating the documentation. Beyond these issues, I found myself moving or copying large chunks of testing code around for relatively small refactors, further fueling my motivation to find a better way.
Upon examining these tests, I noticed that all of them were validating the exceptions thrown by methods when given invalid inputs. While these tests were necessary to get 100% test coverage, the return on investment (ROI) [1] was miniscule. I started looking for a way to get a better ROI than these type tests. A great deal of backend languages (e.g. Java, Go) are statically typed: if I type my JavaScript, that would help get rid of all these type tests and give me a much better ROI.
Enter Flow
The tool that I settled on was Flow. Flow is a type annotation system that is added on top of normal JavaScript. It had two strengths that influenced the decision. First, Flow is opt-in on a per-file basis. This means that types can be slowly added to a project over time, rather than all at once. Second, it has a ‘weak mode’ which will ignore untyped values, allowing developers to slowly roll out typing to a file and then disable weak mode when everything is typed. These two properties allow a minimal initial investment to get started with Flow, but give the developer a strong confidence in the typing system and the checks it provides.
An example is worth a thousand words. Below is a majorly reduced code snippet from Ponos showing the relevant code for setting up a subscription to a topic queue:
const Immutable = require('immutable')
class RabbitMQ {
// This is the way Flow defines instance variable types
subscribed: Set<string>;
subscriptions: Map<string, Function>;
constructor () {
this.subscriptions = new Immutable.Map()
this.subscribed = new Immutable.Set()
}
subscribeToQueue (queue: string, handler: Function): Promise<void> {
if (this.subscribed.has(`queue:::${queue}`)) {
log.warn('already subscribed to queue')
return Promise.resolve()
}
return this.channel.assertQueue(queue)
.then(() => {
this.subscriptions = this.subscriptions.set(queue, handler)
this.subscribed = this.subscribed.add(`queue:::${queue}`)
})
}
}
Flow gives us very quick insight into what variables are being used and their types. In this case, we can quickly see that when we create a new instance of RabbitMQ, we are creating a new string set (subscribed
) as well as a string-to-function map (subscriptions
). Finally, when we start looking at subscribeToQueue
, we can see that it takes a string and a function as its parameters. However, there are no type checks in the implementation! If we use subscribeToQueue
in any other way in the rest of the code base, Flow will return an error saying that the types are incompatible. This allows us to trust Flow to validate all the types we are passing, enabling us to safely delete all the unit tests that were maintained to test type handling.
To illustrate the error reporting, I changed the subscribeToQueue
call to pass an object (instead of a function) as the second parameter.
return this._rabbitmq.subscribeToQueue(
queue,
{
handler: (job, done) => { /* ... */ },
opts: {}
}
)
This results in the following error from Flow:
src/rabbitmq.js:200
200: handler: Function,
^^^^^^^^ function type. Callable signature not found in
232: {
^ object literal. See: src/server.js:232
Flow is showing me in my typed source where I am making a mistake! I replace the object with just the handler function and the error is resolved.
External Modules
One thing that I skipped over is how Flow knew about the immutable
library. This is actually one of the parts where Flow can be a little cumbersome. Roughly, for every external module, you need to create an interface file that tells Flow what types to expect. Flow describes this a bit in their documentation, but I’ll give you a rather complicated example of the interface file for this Set
class, with a little commentary to explain what’s happening.
Immutable is a rather large library that extends many classes to create inheritance with its typing. Below is a condensed slice that gives us the functionality of the Set
used above.
declare class Set {
// this actually inherits from SetCollection, but the two functions
// we use are:
add<U>(value: U): Set<T|U>;
has(key: K): boolean;
}
declare module 'immutable' {
declare var exports: {
Set: Class<Set>
}
}
export {
Set
}
Starting from the bottom, we can see it is exporting the class Set
. This is how we can use Set<string>
as the type for subscribed when we initially define the type in the class. In the constructor of RabbitMQ we actually call new Set()
. This is enabled by the middle block in the snippet; declare module
defines what is actually available from the module. Finally, at the top, we see the declaration of a typed Set
. It has two functions, add
and has
, with defined input and output types.
All of this comes together when flow
is run against our repository. Flow checks all the places where these functions are being used and makes sure all the types are as expected. If we trust Flow, we can then remove all our internal type checking unit tests and simply maintain the much more valuable typing in our code.
Moving Forward
Typing definitely isn’t new, but it’s very new for JavaScript. There are a couple other ways one can get typing for JavaScript. First, TypeScript can be transpiled to JavaScript. TypeScript writes very similarly to Flow’d JavaScript but does have some additional functionality available on top of its defined types. Second, though it will take time, future drafts of ECMAScript are considering including typing directly in JavaScript. This will remove the dependency on anything external for checking types and improve the JavaScript ecosystem.
Until the time comes that JavaScript typing is native, I highly suggest checking out these ways of typing your JavaScript and improving confidence in your code!