Varianza descrive come il subtypes di un tipo composito si relaziona ai subtypes delle sue parti — ovvero, quando Container<Sub> è assegnabile a Container<Super>?
Sia Dog un sottotipo di Animal.
Covarianza — preserva la direzione (il caso intuitivo)
let dogs: Dog[] = [];
let animals: Animal[] = dogs; // ✅ Dog[] is assignable to Animal[]
I tipi di ritorno e gli array sono covarianti: se Dog ⊆ Animal, allora Dog[] ⊆ Animal[]. Una funzione che ritorna Dog è usabile dove è previsto il ritorno di Animal.
Controvarianza — inverte la direzione (parametri di funzione)
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
I parametri di funzione sono controvarianti: Handler<Animal> è assegnabile a Handler<Dog>, l'inverso della relazione tra elementi. Questo è corretto — qualcosa che gestisce tutti gli animali certamente gestisce i cani.
Il default unsound per i metodi
// Method parameters in TS are bivariant by default (a known unsound convenience)
interface Comparer<T> { compare(a: T): void; }
TypeScript controlla i tipi di funzione standalone controvariantly solo sotto strictFunctionTypes; i parametri dei metodi sono intenzionalmente bivarianti per ergonomia, il che è tecnicamente unsound.
Perché è importante
La varianza spiega perché certe assegnazioni sono consentite o rifiutate — perché Dog[] si adatta a Animal[] ma un callback (d: Dog) => void non può sempre stare al posto di (a: Animal) => void.
Comprenderlo aiuta a progettare API generiche (ad es. posizioni di sola lettura vs scrittura) e decodificare confusi errori "non assegnabile" che coinvolgono funzioni e generici.
