Contravariance just means "varies in the opposite direction" (and covariance just means "varies in the same direction"). In the context of subtype relationships, it refers to cases when a compound type is a subtype of another type if-and-only-if one its parts is a supertype of the same part in the other type.
By "compound type" I just mean a type that has other component types. Languages like Haskell, Scala, and Java handle this by declaring that a type has parameters (Java calls this "generics"). From a brief look at the link to the Flow docs, it looks like Flow doesn't formalise parameters, and is effectively considering the type of each property to be a separate parameter. So I'll avoid specifics and just talk about types that are made up of other types.
Subtyping is all about substitutability. If someone wants a T
, I can give them a value of any subtype of T
, and nothing will go wrong; the things they're "allowed" to do with the thing they asked for are only the things that are valid to do with any possible T
. The variance comes in when the types have substructure of other types. If someone asks for a type with a structure that includes a component type T
, and I want to give them a value with a type that has the same structure but the component type is S
, when is that valid?
If the component type is there because they can obtain T
values using the object they're asking for (like reading a property, or calling a method that returns T
values), then when I give them my value they'll be getting S
values out of it instead of the T
values they were expecting. They'll want to do T
ish things with those values, which will only work if S
is a subtype of T
. So for the compound type I have to be a subtype of the one they want, the component of the one I have must be a subtype of the component in the one they want. This is covariance.
On the other hand, if the component type is there because they can send T
values to the object they're asking for (like writing a property, or calling a method that takes T
values as arguments), then when I give them my value it will be expecting them to send it S
values instead of T
ones. My object will want to do S
ish things with the T
values the other person is going to send to it. That's only going to work if T
is a subtype of S
. So in this case, for the compound type I have to be a subtype of the one they want, the component of the one I have must be a supertype of the component in the one they want. This is contravariance.
Simple function types are a concrete example that is generally easily understood with a little thought. A function type written in Haskell notation is like ArgumentType -> ResultType
; this itself is a compound type with two component types, so we can ask whether one function type can be substituted for (is a subtype of) another function type.
Lets say I've got an list of Dog
values, and I need to map a function over it to turn it into a list of Cat
values. So the function that does the mapping is expecting me to give it a function of type Dog -> Cat
.
Can I give it a function of type GreyHound -> Cat
? No; the mapping function will call my function on all of the Dog
values in the list, and we don't know that they are all GreyHound
values.
Can I give it a function of type Mammal -> Cat
? Yes; my function can only do things that are valid for any Mammal
, which obviously includes all of the Dog
values in the list that it will be called on.
Can I give it a function of type Dog -> Siamese
? Yes; the mapping function will use the Siamese
values returned by this function to build a list of Cat
, and Siamese
values are Cat
values.
Can I give it a function of type Dog -> Mammal
? No; this function might turn a Dog
into a Whale
, which won't fit in the list of Cat
the mapping function needs to build.
flowtype
, which barely has over 1k followers. In the same vein, people who know Haskell in general know a lot more about type theory. – Relational