Variance: Covariance & Contravariance
📖 Concept
Variance describes how type relationships (subtype/supertype) are preserved when types are used inside generic containers like Array<T>, Promise<T>, or function parameters.
Covariance (out T) — Preserves the direction: if Dog extends Animal, then Array<Dog> is assignable to Array<Animal>. The subtype relationship is preserved. Output/return positions are covariant.
Contravariance (in T) — Reverses the direction: if Dog extends Animal, then a function (animal: Animal) => void is assignable to (dog: Dog) => void. Input/parameter positions are contravariant.
Invariance — The relationship doesn't hold in either direction. Mutable containers are invariant in strictly-typed languages (but TypeScript is covariant for arrays by design, which is technically unsound but pragmatic).
Why this matters:
- Understanding variance prevents subtle bugs with generic types
- It explains why certain assignments fail with strict function types
- Library authors must understand variance for correct generic constraints
TypeScript 4.7+ in/out annotations let you explicitly declare variance of type parameters, improving type-checking performance and correctness.
🏠 Real-world analogy: Covariance is like a "pets allowed" sign — if dogs are pets, a "dogs allowed" area qualifies as "pets allowed." Contravariance is like a veterinarian — a vet that can treat ANY animal can certainly treat dogs (wider input is compatible with narrower requirement). A vet that only treats fish cannot replace a general vet.
💻 Code Example
1// Setup: type hierarchy2interface Animal { name: string; }3interface Dog extends Animal { breed: string; }4interface Cat extends Animal { indoor: boolean; }56// COVARIANCE — return/output position7// Dog extends Animal → (() => Dog) extends (() => Animal)8type AnimalFactory = () => Animal;9type DogFactory = () => Dog;1011const makeDog: DogFactory = () => ({ name: "Rex", breed: "Lab" });12const makeAnimal: AnimalFactory = makeDog; // ✅ Covariant!13// A function that returns a Dog can be used where Animal is expected1415// CONTRAVARIANCE — parameter/input position (with strictFunctionTypes)16// Dog extends Animal → ((a: Animal) => void) extends ((d: Dog) => void)17type AnimalHandler = (animal: Animal) => void;18type DogHandler = (dog: Dog) => void;1920const handleAnimal: AnimalHandler = (a) => console.log(a.name);21const handleDog: DogHandler = handleAnimal; // ✅ Contravariant!22// A handler for ANY animal can handle a Dog23// const reverseHandleDog: AnimalHandler = handleDog; // ❌ Not safe!2425// Why contravariance for parameters is SAFE:26function feedAnimal(handler: (a: Animal) => void) {27 handler({ name: "Mystery Animal" }); // Might not be a Dog!28}29// If we allowed DogHandler here, handler would expect .breed — crash!3031// Array covariance (TypeScript is pragmatically unsound here)32const dogs: Dog[] = [{ name: "Rex", breed: "Lab" }];33const animals: Animal[] = dogs; // ✅ TypeScript allows this34// animals.push({ name: "Kitty" }); // 😱 No breed! Runtime bug35// This is a known unsoundness in TypeScript — pragmatism over purity3637// TypeScript 4.7+ explicit variance annotations38interface Producer<out T> { // Covariant — only outputs T39 get(): T;40}41interface Consumer<in T> { // Contravariant — only inputs T42 accept(value: T): void;43}44interface Processor<in out T> { // Invariant — both inputs and outputs T45 process(value: T): T;46}4748// Practical: event handler variance49interface EventMap {50 click: MouseEvent;51 keydown: KeyboardEvent;52}5354// Contravariant parameter: handler for Event (base) works for MouseEvent55type BaseHandler = (e: Event) => void;56type ClickHandler = (e: MouseEvent) => void;5758const onClick: ClickHandler = (e) => console.log(e.clientX);59const onEvent: BaseHandler = (e) => console.log(e.type);60// const clickFromBase: ClickHandler = onEvent; // ✅ Safe in strict mode
🏋️ Practice Exercise
Mini Exercise:
- Create a type hierarchy (Animal → Dog → GoldenRetriever) and test covariant/contravariant assignments
- Write a
Producer<T>interface (covariant) and aConsumer<T>interface (contravariant) - Demonstrate why array covariance can be unsafe by adding a Cat to a
Dog[]aliased asAnimal[] - Use
in/outvariance annotations (TS 4.7+) on your generic types - Explain why
(a: Animal) => voidis assignable to(d: Dog) => voidbut not the reverse
⚠️ Common Mistakes
Thinking covariance is always safe —
Dog[] as Animal[]allows pushing non-Dogs, which is unsound; TypeScript allows it for pragmatismNot understanding why function parameters are contravariant — a handler for ANY animal can handle a Dog, but a Dog handler can't handle ANY animal
Confusing variance direction — 'co' means 'same direction' (output), 'contra' means 'opposite direction' (input)
Not enabling
strictFunctionTypes— without it, function parameters are bivariant (both co- and contra-), which is unsoundOver-thinking variance for everyday code — it mostly matters for library authors and generic containers; application code rarely needs explicit variance
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Variance: Covariance & Contravariance