Union Types

Typing values that may be one of many different types

Sometimes it’s useful to create a type which is one of a set of other types. For example, you might want to write a function which accepts a set of primitive value types. For this Flow supports union types.

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow
function toStringPrimitives(value: number | boolean | string) {
  return String(value);
}

toStringPrimitives(1);       // Works!
toStringPrimitives(true);    // Works!
toStringPrimitives('three'); // Works!

// $ExpectError
toStringPrimitives({ prop: 'val' }); // Error!
// $ExpectError
toStringPrimitives([1, 2, 3, 4, 5]); // Error!
object literal This type is incompatible with union: number | boolean | string array literal This type is incompatible with union: number | boolean | string

Union type syntax

Union types are any number of types which are joined by a vertical bar |.

1
Type1 | Type2 | ... | TypeN

You may also add a leading vertical bar which is useful when breaking union types onto multiple lines.

1
2
3
4
5
type Foo =
  | Type1
  | Type2
  | ...
  | TypeN

Each of the members of a union type can be any type, even another union type.

1
2
3
4
type Numbers = 1 | 2;
type Colors = 'red' | 'blue'

type Fish = Numbers | Colors;

Union types requires one in, but all out

When calling our function that accepts a union type we must pass in one of those types. But inside of our function we are required to handle all of the possible types*.

Let’s rewrite our function to handle each type individually.

1
2
3
4
5
6
7
8
9
// @flow
// $ExpectError
function toStringPrimitives(value: number | boolean | string): string { // Error!
  if (typeof value === 'number') {
    return String(value);
  } else if (typeof value === 'boolean') {
    return String(value);
  }
}
string This type is incompatible with an implicitly-returned undefined.

You’ll notice that if we do not handle each possible type of our value, Flow will give us an error.

Unions & Refinements

When you have a value which is a union type it’s often useful to break it apart and handle each individual type separately. With union types in Flow you can “refine” the value down to a single type.

For example, if we have a value with a union type that is a number, a boolean, or a string, we can treat the number case separately by using JavaScript’s typeof operator.

1
2
3
4
5
6
7
// @flow
function toStringPrimitives(value: number | boolean | string) {
  if (typeof value === 'number') {
    return value.toLocaleString([], { maximumSignificantDigits: 3 }); // Works!
  }
  // ...
}

By checking the typeof our value and testing to see if it is a number, Flow knows that inside of that block it is only a number. We can then write code which treats our value as a number inside of that block.

Disjoint Unions

There’s a special type of union in Flow known as a “disjoint union” which can be used in refinements. These disjoint unions are made up of any number of object types which are each tagged by a single property.

For example, imagine we have a function for handling a response from a server after we’ve sent it a request. When the request is successful, we’ll get back an object with a success property which is true and a value that we’ve updated.

1
{ success: true, value: false };

When the request fails, we’ll get back and object with success set to false and an error property describing the error.

1
{ success: false, error: 'Bad request' };

We can try to express both of these objects in a single object type. However, we’ll quickly run into issues where we know a property exists based on the success property but Flow does not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @flow
type Response = {
  success: boolean,
  value?: boolean,
  error?: string
};

function handleResponse(response: Response) {
  if (response.success) {
    // $ExpectError
    var value: boolean = response.value; // Error!
  } else {
    // $ExpectError
    var error: string = response.error; // Error!
  }
}
undefined This type is incompatible with boolean undefined This type is incompatible with string

Trying to combine these two separate types into a single one will only cause us trouble.

Instead, if we create a union type of both object types, Flow will be able to know which object we’re using based on the success property.

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow
type Success = { success: true, value: boolean };
type Failed  = { success: false, error: string };

type Response = Success | Failed;

function handleResponse(response: Response) {
  if (response.success) {
    var value: boolean = response.value; // Works!
  } else {
    var error: string = response.error; // Works!
  }
}

Disjoint unions with exact types

Disjoint unions require you to use a single property to distinguish each object type. You cannot distinguish two different objects by different properties.

1
2
3
4
5
6
7
8
9
10
// @flow
type Success = { success: true, value: boolean };
type Failed  = { error: true, message: string };

function handleResponse(response:  Success | Failed) {
  if (response.success) {
    // $ExpectError
    var value: boolean = response.value; // Error!
  }
}
property `value` Property not found in object type

This is because in Flow it is okay to pass an object value with more properties than the object type expects (because of width subtyping).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @flow
type Success = { success: true, value: boolean };
type Failed  = { error: true, message: string };

function handleResponse(response:  Success | Failed) {
  // ...
}

handleResponse({
  success: true,
  error: true,
  value: true,
  message: 'hi'
});

Unless the objects somehow conflict with one another there is no way to distinguish them.

However, to get around this you could use exact object types.

1
2
3
4
5
6
7
8
9
10
11
12
13
// @flow
type Success = {| success: true, value: boolean |};
type Failed  = {| error: true, message: string |};

type Response = Success | Failed;

function handleResponse(response: Response) {
  if (response.success) {
    var value: boolean = response.value;
  } else {
    var message: string = response.message;
  }
}

With exact object types, we cannot have additional properties, so the objects conflict with one another and we are able to distinguish which is which.