TypeScript Variance — Complete Guide
In this tutorial, you will learn about TypeScript Variance. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript variance describes how subtyping relationships propagate through generic types — whether a List<Dog> is a subtype of List<Animal>, and why function parameters have the opposite direction from function return types.
What You'll Learn
- Covariance: preserving subtype direction
- Contravariance: reversing subtype direction
- Invariance: no relationship preserved
- Bivariance in function types (with
strictFunctionTypes)
Why It Matters
Variance determines what assignments are type-safe. Getting variance wrong leads to runtime errors — like putting a Cat into a list of Dog because the list was typed as Animal[]. Understanding variance helps you design sound generic APIs.
Real-World Use
The Durga Antivirus Pro plugin system uses generic event emitters with proper variance. Event handlers are contravariant in their parameter types — a handler for Event can safely handle MouseEvent, but not vice versa. Getting this wrong would cause runtime crashes in the plugin system.
Learning Path
flowchart LR A[Recursive Types] --> B[Variance] B --> C[Overloads] B --> D[You Are Here] C --> E[Branded Types] E --> F[tsconfig Deep Dive]
What Is Variance?
Variance answers the question: if Dog extends Animal, what is the relationship between Wrapper<Dog> and Wrapper<Animal>?
class Animal { name: string = ""; }
class Dog extends Animal { breed: string = ""; }
class Cat extends Animal { color: string = ""; }
Covariance
Covariance preserves the subtype direction: if Dog is a subtype of Animal, then Wrapper<Dog> is a subtype of Wrapper<Animal>.
// Arrays are covariant in TypeScript (with caveats)
const dogs: Dog[] = [new Dog(), new Dog()];
const animals: Animal[] = dogs; // OK — covariant
// This compiles but causes runtime issues:
animals.push(new Cat()); // Valid at compile time, but dogs now has a Cat!
TypeScript arrays are covariant for practical reasons (most array operations are read-heavy), but this is unsound — you can push incompatible types.
Covariant Generics
interface Producer<T> {
produce(): T;
}
class DogProducer implements Producer<Dog> {
produce(): Dog { return new Dog(); }
}
const producer: Producer<Animal> = new DogProducer(); // OK — covariant
const animal = producer.produce(); // Returns Animal, actually Dog — safe
Covariance is safe for read-only (output) positions.
Contravariance
Contravariance reverses the subtype direction: if Dog is a subtype of Animal, then Consumer<Animal> is a subtype of Consumer<Dog>.
interface Consumer<T> {
consume(value: T): void;
}
const animalConsumer: Consumer<Animal> = {
consume(value: Animal) { console.log(value.name); },
};
const dogConsumer: Consumer<Dog> = animalConsumer; // OK — contravariant
dogConsumer.consume(new Dog()); // Works — Dog has .name
Why does this direction make sense? A consumer that accepts Animal can definitely handle Dog (because Dog is an Animal). But a consumer that accepts only Dog cannot handle any Animal.
Function Parameters Are Contravariant (with strictFunctionTypes)
declare let f1: (x: Animal) => void;
declare let f2: (x: Dog) => void;
f1 = f2; // Error with strictFunctionTypes — unsafe
// f2 accepts Dog only, but f1 could be called with Cat
f2 = f1; // OK — f1 accepts any Animal, Dog is an Animal
Invariance
Invariance means there is no subtype relationship: Wrapper<Dog> is neither a subtype nor a supertype of Wrapper<Animal>.
// Mutable containers should be invariant
interface Container<T> {
get(): T;
set(value: T): void;
}
const dogContainer: Container<Dog> = {
_value: new Dog(),
get() { return this._value; },
set(v: Dog) { this._value = v; },
};
// const animalContainer: Container<Animal> = dogContainer; // Error — should be invariant
// animalContainer.set(new Cat()); // Would corrupt dogContainer
Bivariance
Bivariance allows both directions: Wrapper<Dog> is both a subtype and a supertype of Wrapper<Animal>. TypeScript's function types are bivariant by default (without strictFunctionTypes).
// Without strictFunctionTypes
let f1: (x: Animal) => void;
let f2: (x: Dog) => void;
f1 = f2; // OK — bivariant (unsafe)
f2 = f1; // OK — bivariant (safe)
strictFunctionTypes
Enable strictFunctionTypes: true (included in strict) to make function parameters contravariant (sound):
// With strictFunctionTypes: true
function processDogCallback(cb: (dog: Dog) => void): void {
cb(new Dog());
}
const animalCallback = (animal: Animal) => {
console.log(animal.name);
};
processDogCallback(animalCallback); // OK — parameter is contravariant
// animalCallback accepts Animal, Dog is Animal, so it's safe
Variance in Practice
Readonly/Immutable Types Are Covariant
interface ReadonlyList<T> {
readonly [index: number]: T;
length: number;
}
const dogs: ReadonlyList<Dog> = [new Dog()];
const animals: ReadonlyList<Animal> = dogs; // Safe — no mutation possible
Mutable Types Should Be Invariant
interface Array<T> {
push(value: T): void;
pop(): T | undefined;
readonly length: number;
[index: number]: T;
}
// Arrays are covariant in practice (for convenience), not invariant
Function Types Are Contravariant in Parameters
type EventHandler<T> = (event: T) => void;
const handleAnyEvent: EventHandler<Event> = (e) => console.log(e.type);
const handleClickEvent: EventHandler<MouseEvent> = handleAnyEvent; // OK
Common Mistakes
1. Relying on Array Covariance
const dogs: Dog[] = [new Dog()];
const animals: Animal[] = dogs;
animals.push(new Cat()); // Compiles, but corrupts dogs array
Use readonly arrays (readonly Dog[] or ReadonlyArray<Dog>) for safe covariance.
2. Ignoring strictFunctionTypes
Without it, function parameters are bivariant, hiding real type errors.
3. Confusing Producer and Consumer Roles
// Producer — output position (covariant)
type Producer<T> = () => T;
// Consumer — input position (contravariant)
type Consumer<T> = (value: T) => void;
4. Making Everything Covariant
If a generic type has both input and output usage, it should be invariant (or split into read/write interfaces).
5. Not Using in and out Annotations
TypeScript 4.7+ supports variance annotations:
interface Producer<out T> { produce(): T; } // Covariant
interface Consumer<in T> { consume(value: T): void; } // Contravariant
Practice Questions
What is covariance? The subtype relationship is preserved:
Wrapper<Dog>is a subtype ofWrapper<Animal>ifDogis a subtype ofAnimal.What is contravariance? The subtype relationship is reversed:
Consumer<Animal>is a subtype ofConsumer<Dog>.Why are function parameters contravariant? A function that accepts
Animalcan safely handleDog(which is an Animal), making the more general handler usable in more specific positions.What does
strictFunctionTypesdo? It makes function parameter types contravariant (sound) instead of bivariant (unsafe by default).
Challenge: Create an EventEmitter<T> where T is the event type, the on(handler: (event: T) => void) method is contravariant in its parameter, and emit(event: T) is covariant. Verify that a handler for Event can be used where MouseEvent is expected.
FAQ
Mini Project: Type-Safe Event System with Variance
// src/event-system.ts
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number; }
interface KeyboardEvent extends Event { key: string; ctrlKey: boolean; }
type EventHandler<T extends Event> = (event: T) => void;
class EventEmitter<T extends Event> {
private handlers: EventHandler<T>[] = [];
// Contravariant in handler parameter
on(handler: EventHandler<T>): void {
this.handlers.push(handler);
}
// Covariant in event value
emit(event: T): void {
for (const handler of this.handlers) {
handler(event);
}
}
off(handler: EventHandler<T>): void {
this.handlers = this.handlers.filter(h => h !== handler);
}
}
// Usage
const mouseEmitter = new EventEmitter<MouseEvent>();
// A handler for any Event can handle MouseEvent (contravariance)
const generalHandler: EventHandler<Event> = (event) => {
console.log(`Event at ${event.timestamp}`);
};
// A handler specific to MouseEvent
const specificHandler: EventHandler<MouseEvent> = (event) => {
console.log(`Mouse at (${event.x}, ${event.y})`);
};
mouseEmitter.on(generalHandler); // OK — contravariant
mouseEmitter.on(specificHandler); // OK
mouseEmitter.emit({ timestamp: Date.now(), x: 100, y: 200 });
// Event at ...
// Mouse at (100, 200)
What's Next
Now explore function and constructor overloads:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/27-recursive-types" >}} | Review recursive types |
| {{< ref "/programming-languages/typescript/29-overloads-hybrid" >}} | Function overloads, constructor overloads |
| {{< ref "/programming-languages/typescript/30-branded-types" >}} | Branded types, nominal typing |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro