Variance
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.