Variance beschrijft hoe subtyping van een samengesteld type zich verhoudt tot subtyping van zijn onderdelen — d.w.z. wanneer is Container<Sub> toewijsbaar aan Container<Super>?
Laat Dog een subtype van Animal zijn.
let dogs: Dog[] = [];
let animals: Animal[] = dogs; // ✅ Dog[] is assignable to Animal[]
Gebruikstypen en arrays zijn covariant: als Dog ⊆ Animal, dan Dog[] ⊆ Animal[]. Een functie die Dog retourneert, kan worden gebruikt waar er een wordt verwacht die Animal retourneert.
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
Functieparameters zijn contravariant: Handler<Animal> is toewijsbaar aan Handler<Dog>, het omgekeerde van de elementrelatie. Dit is geldig — iets dat alle dieren afhandelt, handelt zeker honden af.
// Method parameters in TS are bivariant by default (a known unsound convenience)
interface Comparer<T> { compare(a: T): void; }
TypeScript controleert standalone functietype contravariant alleen onder strictFunctionTypes; methodeparameters zijn opzettelijk bivariant voor ergonomie, wat technisch gezien ongeldig is.
Variance verklaart waarom bepaalde toewijzingen zijn toegestaan of verworpen — waarom Dog[] in Animal[] past, maar een (d: Dog) => void callback niet altijd kan dienen als (a: Animal) => void.
Het begrijpen ervan helpt je generieke API's te ontwerpen (bijv. alleen-lezen vs schrijfposities) en verwarrende "not assignable" fouten met betrekking tot functies en generics te decoderen.