Covariance & Contravariance
Introduction
Covariance and contravariance - two buzzwords from a programming language's world, what do they mean? Let's find out in three steps!
Step 1 - Sets
This topic is not so straightforward so let's start with an intuitive example from life with a little of mathematical knowledge. Think about a vehicle and a motorbike. Vehicle is a superset for a motorbike and motorbike is a subset for a vehicle.
You can see clearly that motorbike set being a subset of vehicle set means that this set is smaller than vehicle superset, but it also is more specific - You can say that motorbike is a vehicle but not every vehicle is a motorbike + You cannot specify vehicle type inside a vehicle set, but you can do that in a motorbike set. Here is a little of TypeScript code to make it clearer:
interface Vehicle {
brand: string;
start: () => void;
}
interface Motorbike extends Vehicle {
// Motorbike has all the properties from Vehicle and some more
numberOfWheels: number;
}
const vehicle: Vehicle = { ... }
const motorbike: Motorbike = { ... }
// validate assignment to different types
const expectVehicle: Vehicle = motorbike // OK
const expectMotorbike: Motorbike = vehicle // Error
Step 2 - HOT concept
Before we jump into covariance and contravariance let's introduce one more concept - HOT which stays for higher order type. HOT is an abstract that takes type as an argument and returns another type. Generics types are HOT.
const simpleVehicle: Vehicle = { ... }
const simpleMotorbike: Motorbike = { ... }
// example of HOT
interface ExampleHOT<T> {
additionalProperty: string
element: T;
fn: (elem: T) => string
}
const complexVehicle: ExampleHOT<Vehicle> = { ... }
const complexMotorbike: ExampleHOT<Motorbike> = { ... }
We use HOT as a kind of function that creates complex types based on simple types provided as a parameter (also known as component). Covariant and contravariant are characteristics describing specific HOTs:
- Covariance - is a characteristic of HOT, which says that relation between subtypes of given type is similar to the relation between subtypes of component.
- Contravariance - is a characteristic of HOT, which says that relation between subtypes of given type is inverted to the relation between subtypes of component.
In TypeScript:
- Type which function takes as parameter is contravariant
- Type which function returns is covariant
Step 3 - Complex Example
To better understand those terms let me create more complex graph and TypeScript code as a description
type Vehicle = {
brand: string
}
type Car = {
brand: string
fuelSource: string
}
type SUV = {
brand: string
fuelSource: string
weight: number
}
type Covariant<T> = () => T
let covariantVehicle: Covariant<Vehicle> = { ... }
let covariantCar: Covariant<Car> = { ... }
let covariantSUV: Covariant<SUV> = { ... }
covariantCar = covariantVehicle // Error
covariantCar = covariantSUV // OK
type Contravariant<T> = (x: T) => void
let contravariantVehicle: Contravariant<Vehicle> = { ... }
let contravariantCar: Contravariant<Car> = { ... }
let contravariantSUV: Contravariant<SUV> = { ... }
contravariantCar = contravariantVehicle // OK
contravariantCar = contravariantSUV // Error
Conclusion
The goal of this text is to introduce and explain mechanics that rule in the world of static typing. This knowledge could be very helpful in organizing functions, data structures, and their interfaces that make static typing possible. This subject lies at the root of languages with types, so it is essential to understand them completely.