Subsets & Subtypes

 

What is a subtype?

A type like number, boolean, or string describes a set of possible values. A number describes every possible number, so a single number (such as 42) would be a subtype of the number type.

If we want to know whether one type is the subtype of another, we need to look at all the possible values for both types and figure out if the other has a subset of the values.

For example, if we had a TypeA which described the numbers 1 through 3, and a TypeB which described the numbers 1 through 5: TypeA would be considered a subtype of TypeB, because TypeA is a subset of TypeB.

1
2
type TypeA = 1 | 2 | 3;
type TypeB = 1 | 2 | 3 | 4 | 5;

Consider a TypeLetters which described the strings: “A”, “B”, “C”, and a TypeNumbers which described the numbers: 1, 2, 3. Neither of them would be a subtype of the other, as they each contain a completely different set of values.

1
2
type TypeLetters = "A" | "B" | "C";
type TypeNumbers =  1  |  2  |  3;

Finally, if we had a TypeA which described the numbers 1 through 3, and a TypeB which described the numbers 3 through 5. Neither of them would be a subtype of the other. Even though they both have 3 and describe numbers, they each have some unique items.

1
2
type TypeA = 1 | 2 | 3;
type TypeB =         3 | 4 | 5;

When are subtypes used?

Most of the work that Flow does is comparing types against one another.

For example, in order to know if you are calling a function correctly, Flow needs to compare the arguments you are passing with the parameters the function expects.

This often means figuring out if the value you are passing in is a subtype of the value you are expecting.

So if I write a function that expects the numbers 1 through 5, any subtype of that set will be acceptable.

1
2
3
4
5
6
7
8
9
10
11
// @flow
function method(val: 1 | 2 | 3 | 4 | 5) {
  // ...
}

declare var numsA:  1 |  2;
declare var numsB: 42 | 75;

method(numsA); // Works!
// $ExpectError
method(numsB); // Error!
number literal `42` This type is incompatible with the expected param type of number enum number literal `75` This type is incompatible with the expected param type of number enum

Subtypes of complex types

Flow needs to compare more than just sets of primitive values, it also needs to be able to compare objects, functions, and every other type that appears in the language.

Subtypes of objects

You can start to compare two objects by their keys. If one object contains all the keys of another object, then it may be a subtype.

For example, if we had an ObjectA which contained the key foo, and an ObjectB which contained the keys foo and bar. Then it’s possible that ObjectB is a subtype of ObjectA.

1
2
3
4
5
6
// @flow
type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };

let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB; // Works!

But we also need to compare the types of the values. If both objects had a key foo but one was a number and the other was a string, then one would not be the subtype of the other.

1
2
3
4
5
6
7
// @flow
type ObjectA = { foo: string };
type ObjectB = { foo: number, bar: number };

let objectB: ObjectB = { foo: 1, bar: 2 };
// $ExpectError
let objectA: ObjectA = objectB; // Error!
object type This type is incompatible with object type object type This type is incompatible with object type

If these values on the object happen to be other objects, we would have to compare those against one another. We need to compare every value recursively until we can decide if we have a subtype or not.

Subtypes of functions

Flow compares two functions by comparing its inputs and outputs. If all the inputs and outputs are a subset of the other function, then it is a subtype.

1
2
type Func1 = (1 | 2)     => "A" | "B";
type Func2 = (1 | 2 | 3) => "A" | "B" | "C";

This also applies to the number of parameters in the functions. If one function contains a subset of the parameters of the other, then the other is a subtype.

1
2
3
4
5
6
// @flow
type Func1 = (number) => void;
type Func2 = (number, string) => void;

let func1: Func1 = (a: number) => {};
let func2: Func2 = func1;