Testing plays an important role in the development of any product. As your application becomes more complex and you continue adding new features, you need to verify that your additions haven’t introduced bugs to your existing functionality. We often prefer to write code that can be easily tested but in those cases where the code hasn’t been written with testing in mind, our duty is to “make things testable”. In both these approaches we are forced to write some extra code. Flow promises to solve this problem as far as type checking is concerned.
Flow is a static type checker for JavaScript first introduced by Facebook at the Scale Conference in 2014. It was conceived with a goal of finding errors in JavaScript code, often without messing with our actual code, hence consuming little effort from the programmer. At the same time it also adds type syntax to JavaScript that provides more control to the developers.
In this article, I’ll introduce you to Flow and its main features. All the code reported in this article are available on GitHub.
Installation
Flow currently works on Mac OS X and Linux (64 bit). The installation steps for both can be summarized as follows:
- Download the package for Mac OS X or Linux depending upon your operating system and unzip it
- From the unzipped folder, add the folder containing the executable binary to the system path by executing the commands reported below.
$ cd flow
$ echo -e "\nPATH=\"\$PATH:$(pwd)/\"" >> ~/.bashrc && source ~/.bashrc
Alternatively, Mac OS X users can install it using Homebrew by running on the terminal the command:
$ brew install flow
It will be automatically added to the path.
Once the installation is done, we’re ready to go ahead and explore its features.
Getting Started
A configuration file named .flowconfig
must be present at the root of the project folder. Once this file is present, you can run ad-hoc checks on the code within it and its subfolders by running on the terminal:
flow check
However, this is not the most efficient way to use Flow since it causes Flow itself to recheck the entire project’s file structure every time. We can use the Flow server, instead.
The Flow server checks the file incrementally which means that it only checks the part that has changed. The server can be started by running on the terminal the command flow
.
The first time you run this command, the server will start and show the initial test results. This allows for a much faster and incremental workflow. Every time you want to know the test results, run flow
on the terminal. After you’re done with your coding session, you can stop the server using flow stop
.
Flow’s type checking is opt-in. This means that you don’t need to check all your code at once. You can select the files you want to check and Flow will do the job for you. This selection is done by adding @flow
as a comment at the top of the JavaScript file which you want to be checked by Flow:
/*@flow*/
This helps a lot when you’re trying to integrate Flow into an already existing project as you can choose the files that you want to check one by one and solve their errors.
Type inference
Generally, type testing can be done in two ways:
- We specify to the tool the types we expect, and it checks the code based on those expectations
- The tool is smart enough to deduce the expected type by itself and checks the code based on that
In the first case we write some extra code which is only useful until testing is stripped off from the final JavaScript file that will be loaded in the browser. This requires a bit of extra work that we do in TypeScript. As it turns out, we have to “make the code testable” by adding those extra type annotations. On the other hand, in the second case the code is already ready for being tested without any modification, hence minimizing the programmer’s effort.
Flow falls in this category of tools. It doesn’t force you to change how you code as it automatically deduces the data type of the expressions. This feature is known as type inference and is one of the most important features of Flow.
To illustrate this feature, we can take the below code as an example :
/*@flow*/
function foo(x) {
return x.split(' ');
}
foo(34);
This code will give an error on the terminal when you run the flow
command as the function foo()
expects a string while we have passed a number as an argument. Specifically, the error will be:
/Users/ritz078/projects/flow-examples/getting-started/app.js:4:10,21: call of method split
Property not found in
/private/var/folders/vt/2dfx7m4n451_vjtc9xspgxzw0000gn/T/ flow/flowlib_39612e94/lib/core.js:70:1,87:1: Number
It clearly states the location and the cause of the error. As soon as we change the argument from a number to any string, as shown in the following snippet, the error will disappear.
/*@flow*/
function foo(x) {
return x.split(' ');
};
foo('Hello World!');
As I said, the above code won’t give any error. What we can see here is that Flow understood that the split()
method is only applicable to a string
, so it expected x
to be a string
.
Nullable Types
Flow treats null
in a different way compared to other type systems. It doesn’t ignore null
, thus it prevents errors that may crash the application when null
is passed instead of some other valid types. Consider the following code:
/*@flow*/
function stringLength (str) {
return str.length;
}
var length = stringLength(null);
In the above case, Flow will throw an error. To fix this, we’ll have to handle null
separately as shown below:
/*@flow*/
function stringLength (str) {
if(str !== null){
return str.length;
}
return 0;
}
var length = stringLength(null);
We introduce a check for null
ensuring that the code works correctly in all cases. Flow will consider this last snippet as a valid code.
Type Annotations
One of the best features of Flow is type inference. We don’t have to write type annotations, but in some cases we may need to use them and Flow even provides us that luxury. Consider the following code:
/*@flow*/
function foo(x, y){
return x + y;
}
foo('Hello', 42);
Flow won’t find any error in the above code because the +
(plus) operator is perfectly acceptable on string
s and number
s, and we didn’t specify that the parameters of add()
must be number
s. In that case we use type annotations to specify the desired behavior. Type annotations are prefixed by the :
(colon) sign and they can be placed on function parameters, function return types, and variable declarations. If we add type annotation to the above code, it becomes as reported below:
/*@flow*/
function foo(x : number, y : number) : number{
return x + y;
}
foo('Hello', 42);
This code shows an error because the function expects number
s as arguments while we’re providing a string. The error shown on the terminal will look like the following:
/Users/ritz078/projects/flow-examples/type-annotation/app_annotated.js:7:1,16: function call
Error:
/Users/ritz078/projects/flow-examples/type-annotation/app_annotated.js:7:5,11: string
This type is incompatible with
/Users/ritz078/projects/flow-examples/type-annotation/app_annotated.js:3:17,22: number
If we pass a number instead of 'Hello'
, there won’t be any error. Type annotations is also useful in large and complex JavaScript files to specify the desired behavior.
With the previous example in mind, we can discuss the different annotations supported by Flow. We’ll break the code from app_all_annonated.js file included in the repository into smaller snippets and understand the various type annotations supported by Flow.
/*@flow*/
/*--------- Type annotating a function --------*/
function add(x : number, y : number) : number {
return x + y;
}
add(3, 4);
The above code shows the annotation of a variable and a function. The arguments of the add()
function, as well as the value returned, are expected to be numbers. If we pass any other data type, Flow will throw an error.
Let’s now consider the following snippet:
/*-------- Type annonating an array ----------*/
var foo : Array<number> = [1,2,3];
Array annotations are in the form of Array<T>
where T
denotes the data type of individual elements of the array. In the above code, foo
is an array whose elements should be numbers.
An example schema of class and object is given below. The only aspect to keep in mind is that we can perform an OR operation among two types using the |
symbol. The variable bar1
is annotated with respect to the schema of the Bar
class.
/*-------- Type annonating a Class ---------*/
class Bar{
x:string; // x should be number
y:string | number; // y can be either a string or a number
constructor(x,y){
this.x=x;
this.y=y;
}
}
var bar1 : Bar = new Bar("hello",4);
/*--------- Type annonating an object ---------*/
var obj : {a : string, b : number, c: Array<string>, d : Bar} = {
a : "hello",
b : 42,
c : ["hello", "world"],
d : new Bar("hello",3)
}
We can annotate an object in a similar way to the one reported above.
Null
Any type T
can be made to include null
/undefined
by writing ?T
instead of T
as shown below:
/*@flow*/
var foo : ?string = null;
In this case, foo
can be either a string or null
.
Interfaces
We often face a situation when we have to use a library methods in our code. Flow will throw an error in this case, but usually we don’t want to see those errors. In order to achieve this task, we don’t need to touch the library code. Instead we have to define an interface to the library in a separate file known as interface.js
. It’s just a fancy word for a JavaScript file that contains the declarations of the functions or the methods provided by third-party code.
Let’s take an example to better understand what we’re discussing:
/* @flow */
var users = [
{ name: 'John', designation: 'developer' },
{ name: 'Doe', designation: 'designer' }
];
function getDeveloper() {
return _.findWhere(users, {designation: 'developer'});
}
This code will give the following error:
$ flow-examples/interfaces/app.js:9:10,10: identifier _
Could not resolve name
The error is generated because Flow knows anything about the _
variable/module. To fix this issue we need to bring in an interface file for Underscore.
/*Interface file*/
declare class Underscore {
findWhere<T>(list: Array<T>, properties: {}): T;
}
declare var _: Underscore;
This only describes the interface (or part of it) for Underscore, eliding all implementation details. Therefore, Flow hasn’t to understand the Underscore code itself.
To be sure that Flow recognizes the interface file, we can specify the a [libs]
section in our .flowconfig
file and run the command flow
on the terminal.
[libs]
interfaces/
Once done, Flow won’t show any error.
Stripping the Type annotations
We need to strip the type annotations from the JavaScript file before executing it on the browser. This can be done using JSTransform tool or even Babel. We’ll only discuss the first method in this article.
First we need to install JSTransform globally by running:
npm install -g jstransform
Then we can run the transpiler in the background by executing:
jstransform --strip-types --harmony --watch src/ dist/
This command will strip all the types from the files present in the src
folder and store the compiled version in the dist
folder. It’ll also keep watching the src
folder for any changes. The compiled files can be loaded on the browser just like any other JavaScript file.
There is also another method to perform this task inside the browser but it’s not the recommended method for production purpose due to its instability.
Conclusions
In this article we discussed about the various features of Flow and how we can integrate it in our project to help us improve the quality of our code.
I hope you enjoyed it. In case you want to play with the examples mentioned in this article, please check out the code on GitHub. Feel free to share your doubts or comments below.
-
Korosh Raoufi
-
http://officialandreascy.blogspot.com/ Andreas Christodoulou