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.