Covariance and contravariance help control how types relate when you use them in functions or collections. They make sure your program stays safe when you replace one type with another.
Covariance and contravariance in Typescript
interface Producer<T> {
produce: () => T;
}
interface Consumer<T> {
consume: (item: T) => void;
}TypeScript uses structural typing and does not have explicit in or out keywords like some languages, but it supports covariance and contravariance in function types.
Return types are usually covariant (you can return a subtype), and parameter types are contravariant (you can accept a supertype).
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
// Covariance example:
let getAnimal: () => Animal;
let getDog: () => Dog = () => ({ name: 'Rex', bark: () => console.log('Woof') });
getAnimal = getDog; // Allowed: Dog is subtype of Animaltype Animal = { name: string };
type Dog = { name: string; bark: () => void };
// Contravariance example:
let feedDog: (dog: Dog) => void;
let feedAnimal: (animal: Animal) => void = (animal) => console.log(animal.name);
feedDog = feedAnimal; // Allowed: feedAnimal accepts any Animal, so it can feed a Doglet animals: Animal[] = [{ name: 'Cat' }]; let dogs: Dog[] = [{ name: 'Rex', bark: () => console.log('Woof') }]; // Arrays are covariant in TypeScript animals = dogs; // Allowed but can be unsafe if you add a Cat to animals later
This program shows covariance by assigning a function returning Dog to a variable expecting a function returning Animal. It also shows contravariance by assigning a function accepting Animal to a variable expecting a function accepting Dog.
type Animal = { name: string };
type Dog = { name: string; bark: () => void };
function getAnimal(): Animal {
return { name: 'Generic Animal' };
}
function getDog(): Dog {
return { name: 'Rex', bark: () => console.log('Woof') };
}
let animalProducer: () => Animal;
let dogProducer: () => Dog = getDog;
// Covariance: dogProducer can be assigned to animalProducer
animalProducer = dogProducer;
console.log(animalProducer().name); // Prints 'Rex'
function feedAnimal(animal: Animal) {
console.log(`Feeding ${animal.name}`);
}
let dogFeeder: (dog: Dog) => void;
// Contravariance: feedAnimal can be assigned to dogFeeder
dogFeeder = feedAnimal;
dogFeeder({ name: 'Buddy', bark: () => console.log('Woof') });Covariance means you can use a more specific type where a general type is expected, usually in return types.
Contravariance means you can use a more general type where a specific type is expected, usually in function parameters.
TypeScript checks these rules mostly in function types to keep your code safe.
Covariance lets you replace a type with a more specific one safely, mainly in outputs.
Contravariance lets you replace a type with a more general one safely, mainly in inputs.
Understanding these helps you write flexible and safe TypeScript code with functions and collections.