Variance (phương sai) mô tả mối quan hệ giữa subtyping của một composite type với subtyping của các thành phần của nó — tức là khi nào Container<Sub> có thể gán cho Container<Super>?
Giả sử Dog là một subtype của Animal.
let dogs: Dog[] = [];
let animals: Animal[] = dogs; // ✅ Dog[] có thể gán cho Animal[]
Kiểu trả về và array là covariant: nếu Dog ⊆ Animal, thì Dog[] ⊆ Animal[]. Một function trả về Dog có thể dùng được ở nơi mong đợi một function trả về Animal.
type Handler<T> = (arg: T) => void;
let animalHandler: Handler<Animal> = (a) => {};
let dogHandler: Handler<Dog> = animalHandler; // ✅ (với strictFunctionTypes)
// một handler chấp nhận BẤT KỲ Animal nào có thể xử lý an toàn một Dog
Tham số của function là contravariant: Handler<Animal> có thể gán cho Handler<Dog>, ngược lại với mối quan hệ của phần tử. Điều này là đúng đắn (sound) — thứ xử lý được mọi animal thì chắc chắn xử lý được dog.
// Tham số method trong TS mặc định là bivariant (một sự tiện lợi không đúng đắn đã được biết đến)
interface Comparer<T> { compare(a: T): void; }
TypeScript chỉ kiểm tra các kiểu function độc lập theo kiểu contravariant khi bật strictFunctionTypes; tham số của method được cố ý để bivariant vì lý do tiện dụng, điều này về mặt kỹ thuật là không đúng đắn (unsound).
Variance giải thích tại sao một số phép gán được cho phép hoặc bị từ chối — tại sao Dog[] vừa với Animal[] nhưng một callback (d: Dog) => void không phải lúc nào cũng thay thế được cho (a: Animal) => void.
Việc hiểu nó giúp bạn thiết kế các generic API (ví dụ: vị trí chỉ-đọc so với vị trí ghi) và giải mã các lỗi "not assignable" khó hiểu liên quan đến function và generic.