Introduction to Flow: Flow Types and Declarations

January 2018 ยท 10 minute read

Types and Declarations

I have elected to skip part 1 which discusses the installation and configuration of Flow. I found neither the presentation nor the content compelling or even worthwhile. A lot of it dealt with “potential issues” which you may or may not encounter for which a simple Google search would yield you the answer. I encourage you to mess with each code snippet in the Flow online editor.


And so, let’s begin actually getting into Flow and dissecting the types that are given to us.

Inferred and Declared Types

Let’s assume that you are coming from nowhere with absolutely no prior knowledge of how types work. The first thing that you notice is that adding // @flow to a file will make it start complaining. A completely random example with little revelancy to the real world, let’s take the following function:

// @flow
function divide(a, b) {
    return a / b;
}

const s = divide(8, 5);

If we hover over them to find their types we will find the following:

           number number
               v   v
function divide(a, b) {
    return a / b;
}

const s = divide(8, 5);
      ^
    number

Following that, let’s add another line:

// @flow
function divide(a, b) {
    return a / b;
}

const s = divide(8, 5);
// new line
const ss2 = divide('foo', [1, 3])

Immediately, flow will start complaining:

return a / b;
       ^ string. The operand of an arithmetic operation must be a number.

Why did this happen? Flow recognised that I passed a string into divide() as a first argument and inferred a new type for it.

         number | string
                v
function divide(a,...

At first, Flow knew that a was a number type because it was only used as such. When we also called it with a string, Flow inferred that it could also be a string type and this became problematic since we cannot divide a string.

Then you may figure that this isn’t the problem that you wanted. Your divide() function is fine, the issue is how you are calling it. This is when we will want to move away from inferred types in favor of explicitly declared ones. This is done as follows:

// @flow
function divide(a: number, b: number) {
    return a / b;
}

const s = divide(8, 5);
const ss2 = divide('foo', [1, 3])

Now there error flow will give us is much closer to what we want:

const ss2 = divide('foo', [1, 3])
                    ^ string. This type is incompatible with the expected param type of
function divide(a: number, b: number) {
                    ^ number

As you can see, type inference is great and all but when you have a clear intention, it will usually be better to declare them outright.

Primitive Types:

The same way we declare a parameter type, we can declare a return type:

// @flow
function divide(a: number, b: number): string {
    return a / b;                   /* ^^^^^^ */
}

const s = divide(8, 5);
const ss2 = divide('foo', [1, 3])

Obvious problem:

   return a / b;
            ^ number. This type is incompatible with the expected return type of
function divide(a: number, b: number): string {
                                        ^ string

We can make the return types match in this case by using the .toString() method:

// @flow
function divide(a: number, b: number): string {
    return (a / b).toString();
}

const s = divide(8, 5);
const ss2 = divide(9, 5)

And now all the issues are gone.


If you are under the impression that I am rewriting the documentation, it’s because I am. Sadly, Flow’s documentation is seemingly not written for humanoids.


Literal Types

Consider the following:

// @flow
let a: 2 = 2;

This means the variable a may only contain the literal type 2 and nothing else. This may sound pointless but once we get familiar with Union types and Intersection types, we will see things such as the following:

// @flow
let status: 'success' | 'error' | 'warning' = 'success';

// Error: [flow] string (This type is incompatible with string enum)
status = "something else";

This is often done to do what amounts to an enum where a variable of a certain type may only hold a specific subset of values. The | operator designates an OR but for types. With it, we can do something as follows:

// @flow
let data: string | number = 5;

// This works
data = "Hello World!"

// This does not
data = false

Mixed and Any Types

There are two other important types which I haven’t mentionned yet: mixed and any.

Mixed

Mixed is a composition of all possible types. In other words, mixed is a type which requires a proof of it’s actual type before employing. We will get into the topic of “proving” types in later posts but for now consider the following:

// @flow
declare var s: mixed;
const foo: number = s1 / 8;
                    ^ mixed. The operand of an arithmetic operation must be a number.

That is problematic because while it can be a number, it’s not necessarily one. One of the ways to prove that a variable is a number, is to force it to be one.

// @flow
declare var s: mixed;
const s1 = parseInt(s);
const foo: number = s1 / 8;

Now this will not return any errors as the return type of parseInt() is obviously a number.

In case you are having trouble with the case in which s is a string and are concerned that it cannot be a number, that is a quirk that javascript has which I will explain better in code rather than in words:

>> parseInt("Hello There")
<< NaN
>> typeof NaN
<< "number"
>> typeof parseInt("Hello There")
<< "number"

To consolidate, here is another example:

// @flow
declare var s: mixed;

// Not necessarily an object, not okay
s.toString();

// Proof that it's an object, but it can still be null. Not okay
if (typeof s === 'object')
    s.toString();

// Proof that it's a non-null object. Okay
if (s && typeof s === 'object')
    s.toString();

To view the errors yourself, visit the online flow editor.

In summary, mixed is an undetermined type whose type we aren’t sure of and need to prove.

Any

At first glance, any seems to be the same as mixed. This however, is not the case as any is somewhat an anti-pattern and needs to be avoided as much as possible. What any says is that in that type there might be anything but only one thing such that Flow will not attempt to control it.
To give a better idea of an issue, consider the following:

// @flow
declare var ss: any;
const foo: number = ss;

The above does not produce an error. Any time you use any, you forfeit all type inferrence and checking for that variable. Sadly, any can be found in standard javascript utilities such as JSON.parse() which should produce a mixed instead of even a subset of types.

Maybe types

Sometimes a variable can be a “maybe”. Maybe means either value of a concrete type or null. The notation is as follows:

// @flow
let s: ?string;
s = "123"
s = null

// Will not work because null.includes() is not a method.
s.includes("foo")

// Proved that s is non-null. Therefor a string.
if (s)
    s.includes("foo")

Also note the inferred types:

string | nul
    v
if (s)
    s.includes("foo")
    ^
 string

Who said declare types can’t still be inferred? More on that later on.

Optional arguments

Sometimes you want functions to have optional parameters. This is done by adding a ? before the : when declaring its type.

// @flow
function sum(a: number, b?: number) {
    return a + b
}

// Same for arrow functions
const arrowF = (a: number, b?: number) => {
    return 5
}

This is how Flow handles when you call that function:

// Works
arrowF(1);
arrowF(1, 2);
// Doesn't work
arrowF(1, 2, 3);

Nothing to explain there.

You may also take advantage of the trendy new ... rest operator:

const arrowF = (a: number, b: number, ...rest: Array<number>) => {
    return 5
}

…in which case we could now call arrowF() with as many arguments as we want as long as they are all numbers.

this

In functions, you have access to the this parameter which defines execution context. As we all know, with a bit of magic with .bind() and the such, this can literally be anything.

In Typescript, you could declare this to be a specific type of your choosing. Flow does not allow you to do that.
Why not? As already said, this could be anything and Flow does not trust the programmer to say what it is. Instead, this’ types will always be inferred based on how you call your functions.

Callbacks

One way of typing callbacks if by using the Function type, something which I highly recommend against. The reason being is that Function behaves much like any in the sense that it prevents any type inference.
Instead, it is optimal to declare the callback’s parameters and return type:

// @flow
function process(cb: (err: Object) => void) {
    cb({ error: s})
}

Here’s an example of the aforementionned type inference:

// @flow
function process(cb: (err: Object) => void) {
    cb({ error: 1})
}

process((a: number) => {
    console.log(a)
})
process((a: number) => {
         ^ function. This type is incompatible with the expected param type of
function process(cb: (err: Object) => void) {
                     ^ function type

Working example:

process((a: Object) => {
    console.log(a)
})

Arrays

There exist two ways to declare typed arrays: Generics and Array short-hand.

Array of Type

Generic:

let a: Array<number> = [1, 2, 3, 4]

Short-hand:

let a: number[] = [1, 2, 3, 4]

Array of Type or Null

Generic:

let a: Array<number | null> = [1, 2, 3, null, 4]

Short-hand:

let a: (number | null)[] = [1, 2, 3, null, 4]

Array of Type or Null (Maybe variant)

Generic:

let a: Array<?number> = [1, 2, 3, null, 4]

Short-hand:

let a: (?number)[] = [1, 2, 3, null, 4]

(?number)[] is not equivalent to ?(number)[]. The former means an array of numbers and nulls while the latter means either an array of numbers or null. When omitting the parenthesis, the latter form is assume. To avoid confusion, always use parenthesis in such cases.

Tuples

While no such concept exists in plain Javascript, it may sound familiar to people coming from Python or Rust. In simple terms, it is an array of fixed length of not necessarily related peaces of data.

It is easily the simplest one to grasp:

// @flow
let coords: [number, number, number] = [.3, .5, .7];

// Allowed
coords[1] = 3
coords = [7, 5, 3]

// Not allowed
coords.push(2)
coords = [7, 5, "foo"]
coords = [7, 5]

Interesting detail:

let data: [string, number, number] = [.3, .5, .7];

While .3 can be coerced to be a string '0.3', Flow takes a the strict approach that can be summed up as evident > not evident.

Unsound detail

This is a quirky portion of Flow where it drifts away from it’s sacred struggle for Soundness as outlined in my previous post.

// @flow

declare var a: string[];

let value = a[5];
     ^
    string

Flow infers that the 6th element of a will be a string since it’s an array of strings. This is unsound as it may simple not exist. A more sound type would have been ?string.

Type alias

Type alias is the last thing we are going to look at in this post.

// @flow
type ArrayOfPossibleStrings = (?string)[];
declare var b: ArrayOfPossibleStrings;
const r = b[32]

This is the way you will be referencing types in real projects as to not repeat yourself and render the code more readable.

This post was written for Flow 0.62