Variance describes how subtyping of a composite type relates to subtyping of its parts — i.e. when is Container<Sub> assignable to Container<Super>?
Let Dog be a subtype of Animal.
let dogs: Dog[] = [];
let animals: Animal[] = dogs; // ✅ Dog[] is assignable to Animal[]
Return types and arrays are covariant: if Dog ⊆ Animal, then Dog[] ⊆ Animal[]. A function returning Dog is usable where one returning Animal is expected.
type Handler<T> = (arg: T) => void;
let animalHandler: Handler<Animal> = (a) => {};
let dogHandler: Handler<Dog> = animalHandler; // ✅ (with strictFunctionTypes)
// a handler that accepts ANY Animal can safely handle a Dog
Function parameters are contravariant: Handler<Animal> is assignable to Handler<Dog>, the reverse of the element relationship. This is sound — something that handles all animals certainly handles dogs.
// Method parameters in TS are bivariant by default (a known unsound convenience)
interface Comparer<T> { compare(a: T): void; }
TypeScript checks standalone function types contravariantly only under strictFunctionTypes; method parameters are intentionally bivariant for ergonomics, which is technically unsound.
Variance explains why certain assignments are allowed or rejected — why Dog[] fits Animal[] but a (d: Dog) => void callback can't always stand in for a (a: Animal) => void.
Understanding it helps you design generic APIs (e.g. read-only vs write positions) and decode confusing "not assignable" errors involving functions and generics.