SyntaxStudy
Sign Up
TypeScript Variance, Covariance, and Contravariance
TypeScript Beginner 1 min read

Variance, Covariance, and Contravariance

Variance describes how type relationships between complex types relate to the type relationships of their component types. A type is covariant if it preserves the direction of subtype relationships: if Dog extends Animal, then Array extends Array. A type is contravariant if it reverses the direction: a function accepting Animal is a subtype of a function accepting Dog, because it can handle more types. In TypeScript, function return types are covariant and function parameter types are contravariant. This is the correct and sound behaviour. TypeScript 4.7 introduced explicit variance annotations using in and out keywords on type parameters, which allows the compiler to check variance rather than compute it. This can significantly improve type-checking performance for complex generic types. Understanding variance matters when you design mutable containers and callback-based APIs. Arrays in TypeScript are technically unsound because they are treated as covariant even though mutable arrays should be invariant — this is a known trade-off for practicality. Readonly arrays, however, are correctly covariant because you can only read from them. Recognising these subtleties allows you to design type-safe APIs that do not have hidden type safety holes.
Example
// Covariance — return types
type Producer<T> = () => T;

// Dog is a subtype of Animal
class Animal { name: string = "animal" }
class Dog extends Animal { breed: string = "lab" }

// Producer<Dog> is assignable to Producer<Animal> (covariant)
const dogProducer: Producer<Dog> = () => new Dog();
const animalProducer: Producer<Animal> = dogProducer; // OK

// Contravariance — parameter types
type Consumer<T> = (value: T) => void;

// Consumer<Animal> is assignable to Consumer<Dog> (contravariant)
const animalConsumer: Consumer<Animal> = (a) => console.log(a.name);
const dogConsumer: Consumer<Dog> = animalConsumer; // OK — handles more than needed

// Explicit variance annotations (TypeScript 4.7+)
type CovariantBox<out T>     = { get(): T };
type ContravariantBox<in T>  = { set(value: T): void };
type InvariantBox<T>         = { get(): T; set(value: T): void };

// CovariantBox<Dog> extends CovariantBox<Animal>
const dogBox: CovariantBox<Dog>       = { get: () => new Dog() };
const animalBox: CovariantBox<Animal> = dogBox; // OK

// Method vs function property — different variance
interface WithMethod   { method(s: string): void }   // bivariant (unsound)
interface WithProperty { prop: (s: string) => void } // contravariant (sound)

// Readonly arrays are covariant, mutable arrays are not
const dogs: Dog[] = [new Dog()];
const animals: readonly Animal[] = dogs; // OK (readonly covariance)
// const mutableAnimals: Animal[] = dogs; // Error in strict mode