Introduction to Flow: Union, Intersection, Spread

January 2018 ยท 6 minute read

Union & Intersection

We already briefly touched up these topics when working with literal types in *Introduction to Flow: Flow Types and Declarations.

Refresher:

// @flow
const s: 'success' | 'warning' | 'error' = 'success'

It doesn’t all have to be on one line by the way, it can be spread out over multiple lines as follows:

// @flow
const s: 
    | 'success' 
    | 'warning' 
    | 'error'
    | 'long long'
    | 'very long long' = 
    'success'

That isn’t the way we usually do since we have type aliases:

// @flow
type possibleStatisesType =
    | 'success' 
    | 'warning' 
    | 'error'
    | 'long long'
    | 'very long long';

const status: PossibleStatusType = 'success'

What you see above is what is called a Union, or more specifically, one of the two types of Unions. The other one is a bit more complicated so we’ll get to it after looking at refinment and disjoint unions.

Intersections

Let’s suppose we have a type NameType and HistoryType which are defined as follows:

// @flow
type NameType = {
    name: string
}

type HistoryType = {
    history: string[]
}

Suppose we want to make a third take which combines both of them named User. Since the union type looks a lot like a logical OR, it shouldn’t come as a surprise that the intersection type would like a logical AND:

type User = HistoryType & NameType

The type resulting from the combination looks as follows:

u: {
    history: Array<string>,
    name: string
}

Impossible Types

The intersection means that a variable that uses the User type, must adhere to both the requirements of HistoryType and NameType. You may be thus tempted to try an impossible intersection such as this:

type ImpossibleType = number & string

Flow will allow you to write it as long as you don’t assign actual values to a variable of an impossible type. For example:

type ImpossibleType = number & string
declare var u:ImpossibleType;
// This works as `u` meets the requirements of a `number` by definition
Math.round(u)

// These will not work because a number cannot be a string
u = 5;
u = "5";

Of course, the above is pointless and if you accidentally end up doing something of the sort, Flow will start catching issues early on.

Intersections of Nested Objects

Consider if we add a common property that defines a nested object as follows:

// @flow
type NameType = {
    metadata: {
        originalName: string
    },
    name: string
}

type HistoryType = {
    metadata: {
        historyLimit: number
    },
    history: string[]
}

When intersected, the above would create a new type which combines the nested properties the same way as it does for the non-nested ones.

HistoryType & NameType: {
    metadata: {
        historyLimit: number,
        originalName: string
    },
    history: Array<string>,
    name: string
}

// Alternative representation:
HistoryType & NameType: {
    metadata: { historyLimit: number } & { originalName: string },
    history: Array<string>,
    name: string
}

However, the intersection of an exact type will lead to issues. There is currently weirdness surrounding it and thus I will not document the behavior until it is either addressed in my issue or officially documented. Regardless, do not intersect exact types, it will bring but sadness into your life.

Spread

Spread is the most interesting item in this post as the one that causes the greatest amount of confusion in people new to Flow. It is a relatively new addition and is meant to allow you to do intersections but in more specific ways. Consider the following:

// @flow
type NameType = {
    version: string,
    name: string
}

type HistoryType = {
    version: number,
    history: string[]
}

type UserAnd = NameType & HistoryType

In an unfortunate twist of events, we have a conflict between version either being a string or a number as it must be both, which is impossible. When using the & operator, it is impossible to correct this issue.

However, with the spread syntax, it is possible and works exactly as ES6 spread:

// @flow
type NameType = {
    version: string,
    name: string
}

type HistoryType = {
    version: number,
    history: string[]
}

type User = {
    ...NameType,
    ...HistoryType
}

const x: User = {
    name: 'foo',
    history: ['abc'],
    version: 'bar'
}

In we take the type of x, it will look as follows:

x: {
    history?: mixed | Array<string>,
    name?: string | mixed,
    version? : number | string
}

Where do the mixed come from? Why are all properties optional?
The answer is that an inexact type always has an implied property which makes it inexact:

type NameType = {
    version: string,
    name: string,
    [key: string]: mixed
}

An inexact type may have any amount of arbitrary properties which are indentified by a string and contain any one specific type (i.e. a mixed type).

The next thing is that the way the spread operator works, is every new object’s properties may overwrite the previous ones’. This means is that if NameType ends up having a property named history, it will overwrite it with it’s value which is potential any time (again, mixed).

If at this point you are confused by mixed, go read the relevant section in either my post on the matter or, alternatively, the official documentation. If you are confused by how the spread operator works, refer to MDN docs. You are advised against continuing if either topic escapes you.

We can fix the above issue by making both types exact:

// @flow
type NameType = {|
    version: string,
    name: string
|}

type HistoryType = {|
    version: number,
    history: string[]
|}

type User = {
    ...NameType,
    ...HistoryType
}

const x: User = {
    name: 'foo',
    history: ['abc'],
    version: 'bar'
}

Now, we get a new issue: x.version is expected to be a number, but it is a string. The reason it is expected to be a number is that HistoryType is referenced last and overwrites what NameType said about version being a string. This can be corrected by flipping the order.

type User = {
    ...HistoryType,
    ...NameType
}

However, the solution above will not always be available. For example if both types just so happen to be inexact. We will explore that in depth once we get to Utility Types and Magic types, but for now it will suffice to know that using the magic operator $Exact<T> which will coerce an inexact type and make it exact:

type User = {
    ...$Exact<HistoryType>,
    ...$Exact<NameType>
}

Closing Remarks

Most of the time, you will have a choice between working with intersections and spreads, but I personally find using spreads much more appealing. Intersections will have an important role to play even if you enjoy using spreads as we will see with higher order components in React.

Written for Flow 0.62