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. Conversely, number
would be a supertype of the type 42.
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
(a union of literal types), 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.
1type TypeA = 1 | 2 | 3;2type 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.
1type TypeLetters = "A" | "B" | "C";2type 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.
1type TypeA = 1 | 2 | 3;2type 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 you write a function that expects the numbers 1 through 5, any subtype of that set will be acceptable.
1function f(param: 1 | 2 | 3 | 4 | 5) {2 // ...3}4
5declare const oneOrTwo: 1 | 2; // Subset of the input parameters type.6declare const fiveOrSix: 5 | 6; // Not a subset of the input parameters type.7
8f(oneOrTwo); // Works!9f(fiveOrSix); // Error!incompatible-typeCannot call f with fiveOrSix bound to param because union type [1] is incompatible with union type [2].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, if ObjectA is inexact.
1type ObjectA = {foo: string, ...};2type ObjectB = {foo: string, bar: number};3
4let objectB: ObjectB = {foo: 'test', bar: 42};5let 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.
1type ObjectA = {foo: string, ...};2type ObjectB = {foo: number, bar: number};3
4let objectB: ObjectB = { foo: 1, bar: 2 };5let objectA: ObjectA = objectB; // Error!incompatible-typeCannot assign objectB to objectA because in property foo: number [1] is not exactly the same as string [2].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
Subtyping rules for functions are more complicated. So far, we've seen that A
is a subtype of B if B contains all possible values for A. For functions,
it's not clear how this relationship would apply. To simplify things, you can think
of a function type A as being a subtype of a function type B if functions of type
A can be used wherever a function of type B is expected.
Let's say we have a function type and a few functions. Which of the functions can be used safely in code that expects the given function type?
1type FuncType = (1 | 2) => "A" | "B";2
3declare function f1(1 | 2): "A" | "B" | "C";4declare function f2(1 | null): "A" | "B";5declare function f3(1 | 2 | 3): "A";6
7f1 as FuncType; // Errorincompatible-typeCannot cast f1 to FuncType because in the return value: union type [1] is incompatible with union type [2].8f2 as FuncType; // Errorincompatible-typeCannot cast f2 to FuncType because in the first parameter: union type [1] is incompatible with number literal 2 [2].9f3 as FuncType; // Works!f1can return a value thatFuncTypenever does, so code that relies onFuncTypemight not be safe iff1is used. Its type is not a subtype ofFuncType.f2can't handle all the argument values thatFuncTypedoes, so code that relies onFuncTypecan't safely usef2. Its type is also not a subtype ofFuncType.f3can accept all the argument values thatFuncTypedoes, and only returns values thatFuncTypedoes, so its type is a subtype ofFuncType.
In general, the function subtyping rule is this: a function type B is a subtype
of a function type A if and only if B's inputs are a superset of A's, and B's outputs
are a subset of A's. The subtype must accept at least the same inputs as its parent,
and must return at most the same outputs.
The decision of which direction to apply the subtyping rule on inputs and outputs is governed by variance, which is the topic of the next section.