Many programming languages allow subtyping. E.g., Pizza
is a subtype of Food
, and Cat
is a subtype of Animal
. Often, anywhere that requires an Animal
, you can use a Cat
. It is intuitive for simple types such as Animal
or Food
, but what about complex types such as List<Animal>
, function<Animal>()
or generic types Collection<T>
? Is a List<Cat>
a subtype of List<Animal>
? Different languages have different opinions on the matter. The subtyping between complex types is called variance. It can be classified into four types: covariance, contravariance, invariance and bivariance.
If the subtyping relationship of the underlying simple types is preserved, e.g., โa list of catsโ is a subtype of โa list of animalsโ, we say itโs covariant. Examples include functions
in TypeScript - for a function to be a subtype of another function, its return type has to be a subtype (or the same type) of the other functionโs return type.
When passing in function parameters, it makes more sense to argue that contravariance makes more sense when consuming a type (as function parameters). E.g. every Pizza
is a Food
, but eating every Pizza does not cover eating all types of Food
.
There is often a trade-off between usefulness and safety when deciding the variance relationship of a language. A more nuanced and fine-grained variance hierarchy may lead to unsafe code but could make the language more expressive, useful and often considered well-typed.