Introduction to Flow: Object Typing

January 2018 ยท 6 minute read

Object Typing

Object types is one of the most fundamental topics in Flow if for no other reason than that when working with Javascript, you are usually working mostly with objects.

Declaring Object Types

Declaring an object type in Flow is much like doing so in Javascript:

// @flow
type User = {
    name: string,
    age: number
}

You could also use ; instead of the , but I don’t know why you would want to do such a thing.
Afterwards you can create an object with that type:

// @flow
type User = {
    name: string,
    age: number
}

const x: User = {
    name: "Slava",
    age: 18
}

What happens when we try to work with an undeclared property? Such as

x.specialPowers = true;

Flow begins to complain:

x.specialPowers = true;
  ^ property `specialPowers`. Property not found in
x.specialPowers = true;
^ User

This is a bit of a useless example. Let’s look at something more common.

// @flow
type User = {
    name: string,
    age: number
}

const x: User = {
    name: "Slava",
    age: 18
}

// Silly me, wrote name with 2 'n'
console.log(x.nname);

Typos such as this one will quickly get caught by Flow:

console.log(x.nname);
              ^ property `nname`. Property not found in
console.log(x.nname);
            ^ User

Exact Types and Inexact Types

There exist two notions worth considering when using objects: Exact types and Inexact Types. Consider the following scenario:

// @flow
type User = {
    name: string,
    age: number
}

type LogEntry = {
    name: string
}

const x: User = {
    name: "Slava",
    age: 18
}

function log(entry: LogEntry) {
    return entry.name;
}

log(x)

What we are saying he is that a LogEntry is nothing more than an object with the property name. Does User have name? Yes. That means it can be passed in place where there is a LogEntry type. This is what is called an inexact type.

In some cases, for example when working with MongoDB, you would want to avoid useless data inside your database. Consider the following:

// @flow
type User = {
    name: string,
    age: number
}

function log(entry: User) {
    return entry;
}

log({
    name: "Slava",
    age: 18,
    hasSuperPowers: false
})

The above will not cause issues either. However, we want it to. We do not want hasSuperPowers in our data. This where exact types come in. This is done by putting | in the object type declaration as follows:

// @flow
type User = {|
    name: string,
    age: number
|}

function log(entry: User) {
    return entry;
}

log({
    name: "Slava",
    age: 18,
    hasSuperPowers: false
})

No that we declare User as an exact type, we will get an error for hasSuperPowers:

log({
    ^ object literal. This type is incompatible with the expected param type of
function log(entry: User) {
                           ^ User
        Property `hasSuperPowers` is incompatible:
        log({
            ^ property `hasSuperPowers`. Property not found in
        function log(entry: User) {
                            ^ User

An exact type is an object where all the attributes enumerated must be present and nothing more.

Interesting ceveat:

// @flow
type User = {|
    name: string,
    age: ?number
|}

// Allowed
const a: User = {
    name: "Slava",
    age: undefined
}

// Not Allowed
const b: User = {
    name: "Slava"
}

Even though in both cases age is undefined, it must be explicitly set as undefined. Think of the Object.hasOwnProperty method.

We can however, make it work by setting age as an optional property by putting a ? after the property name:

// @flow
type User = {|
    name: string,
    age?: ?number
|}

// Allowed
const a: User = {
    name: "Slava",
    age: undefined
}

// Allowed
const b: User = {
    name: "Slava"
}

Object Maps

Sometimes, we use objects as maps where our data looks something like this:

let homeCountries = {
    steven: "US",
    slava: "Russia",
    maxime: "Canada"
}

This is possible to type in Flow using a special syntax:

// @flow
type CountriesMap = {
    [person: string]: string
}

let homeCountries: CountriesMap = {
    steven: "US",
    slava: "Russia",
    maxime: "Canada"
}

homeCountries.Adi = "India"

// Error: boolean incompatible with string
homeCountries.countriesValid = true

When doing this, nothing prevents us from declaring other fields:

// @flow
type CountriesMap = {
    [person: string]: string,
    countriesValid: boolean
}
/* ... */
// Valid
homeCountries.countriesValid = true
// Not Valid
homeCountries.countriesValid = "true"

Function Objects

Sometimes, we want to do interesting things with Functions by manipulating them as Objects. For example:

// @flow
function foo() {
    /* ... */
};

foo.dropCache = () => { /* ... */ }

The above is considered valid in Javascript because Functions are Objects too. Now you ask, what is the type of such a function? For such cases, Flow has a special syntax under the guise of the $call property.

// @flow
type FooType = {
    dropCache: () => void,
    $call: (a: number) => number
}

declare var process: FooType;

process(5);
process.dropCache();

In other words, you may consider the $call property as a way to tell flow that a given object is callable.

Sealed Objects and Unsealed Objects

Sealed and Unsealed objects are a very important topic to understand when working with Flow since it is deeply tied to Flow’s type inference system.

const config = {
    dialiect: 'mysql',
    db: 'dsm',
    user: 'root'
}

The above object is something we would call a sealed object because if were to try to add a new property to it, it would fail.

...
// Error
config.foo = true;

However, if our initial config would be an empty object, it would be unsealed:

// @flow
const config = {}
// Okay
config.foo = true;

This is because having an empty object sealed is useless.

Now the thing to learn here, is that when you create an unsealed object Flow has a very unsafe behavior:

// @flow
const config = {}

// No errors
const r: string = config.baz;
                         ^
                        any

This is like the problem with unsound array access and any combined. It both trusts us that the value exists and that the value is of whatever type we claim it to be.
If you need to work with unsealed objects, you need to declare a type for it:

// @flow
const config: {
    [key: string]: number
} = {}

// Error: number is incompatible with string
const r: string = config.baz;
                         ^
                        number

Review

To summarize, throughout this post you should have learned about: