Variance: Covariance & Contravariance

0/5 in this phase0/21 across the roadmap

📖 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

codeTap to expand ⛶
1// Setup: type hierarchy
2interface Animal { name: string; }
3interface Dog extends Animal { breed: string; }
4interface Cat extends Animal { indoor: boolean; }
5
6// COVARIANCE — return/output position
7// Dog extends Animal → (() => Dog) extends (() => Animal)
8type AnimalFactory = () => Animal;
9type DogFactory = () => Dog;
10
11const makeDog: DogFactory = () => ({ name: "Rex", breed: "Lab" });
12const makeAnimal: AnimalFactory = makeDog; // ✅ Covariant!
13// A function that returns a Dog can be used where Animal is expected
14
15// 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;
19
20const handleAnimal: AnimalHandler = (a) => console.log(a.name);
21const handleDog: DogHandler = handleAnimal; // ✅ Contravariant!
22// A handler for ANY animal can handle a Dog
23// const reverseHandleDog: AnimalHandler = handleDog; // ❌ Not safe!
24
25// 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!
30
31// Array covariance (TypeScript is pragmatically unsound here)
32const dogs: Dog[] = [{ name: "Rex", breed: "Lab" }];
33const animals: Animal[] = dogs; // ✅ TypeScript allows this
34// animals.push({ name: "Kitty" }); // 😱 No breed! Runtime bug
35// This is a known unsoundness in TypeScript — pragmatism over purity
36
37// TypeScript 4.7+ explicit variance annotations
38interface Producer<out T> { // Covariant — only outputs T
39 get(): T;
40}
41interface Consumer<in T> { // Contravariant — only inputs T
42 accept(value: T): void;
43}
44interface Processor<in out T> { // Invariant — both inputs and outputs T
45 process(value: T): T;
46}
47
48// Practical: event handler variance
49interface EventMap {
50 click: MouseEvent;
51 keydown: KeyboardEvent;
52}
53
54// Contravariant parameter: handler for Event (base) works for MouseEvent
55type BaseHandler = (e: Event) => void;
56type ClickHandler = (e: MouseEvent) => void;
57
58const 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:

  1. Create a type hierarchy (Animal → Dog → GoldenRetriever) and test covariant/contravariant assignments
  2. Write a Producer<T> interface (covariant) and a Consumer<T> interface (contravariant)
  3. Demonstrate why array covariance can be unsafe by adding a Cat to a Dog[] aliased as Animal[]
  4. Use in/out variance annotations (TS 4.7+) on your generic types
  5. Explain why (a: Animal) => void is assignable to (d: Dog) => void but not the reverse

⚠️ Common Mistakes

  • Thinking covariance is always safe — Dog[] as Animal[] allows pushing non-Dogs, which is unsound; TypeScript allows it for pragmatism

  • Not 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 unsound

  • Over-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