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 is-a of the underlying simple types is preserved, e.g., for Cat and Animal, List<Cat> is a subtype of List<Animal>, 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.

Contravariance is the opposite of covariance. It means that if type B is a subtype of type A, then Container<A> is considered a subtype of Container<B>. Contravariance makes more sense for consuming a value producer (as function parameters). E.g. with feed_people(customer: (items: List<Pizza>)), it is ok to feed_people(customer: (items: List<Food>)) because anyone who can consume Food can consume Pizza, but not the the other way around - people who eat Pizza might not eat other type of Food.

When you have a List<Pizza> (a producer), you can treat it as a List<Food> because anything you pull out is guaranteed to be at least a Food. You’re consuming what the list produces. When you have a Consumer<Food> (a consumer), you can use it where a Consumer<Pizza> is expected because it can accept anything you push into it - including pizzas. The consumer is on the receiving end.

Bivariance is when a type parameter is treated as both covariant and contravariant simultaneously - substitution works in either direction. This is generally considered bad practice, but some languages allow it for pragmatic reasons.

Invariance is when a type parameter allows neither covariance nor contravariance - List<A> and List<B> have no subtype relationship even if A and B do.

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.