DEV Community

Code Oz

Posted on • Updated on

How I understand Covariance & Contravariance in typescript

Covariance, contravariance, bivariance... Theses words seems unfamiliar, difficult to understand for you ?

I promise you, at the end of this article, all of this will be more clearly for you.

What is ?

When you are using class, a class can extend to another class. For example:

``````class Animal {}

class Dog extends Animal {}

class Greyhound extends Dog {}
``````

That means 2 important things :

• Dog is a `subtype` of Animal, and Animal is the `supertype` of Dog.

• Dog is the `supertype` of Greyhound and Greyhound is a `subtype` of Dog

Yes nice and ?

We can now understand the definitions of Covariance, contravariance and bivariance !

Covariance :

Covariance accept `subtype` but doesn't accept `supertype`

We can take a function that will accept only `covariant` type of `Dog`

``````const acceptDogCovariance = function (value: Covariant<Dog>) { ... }

acceptDogCovariance(new Animal()) // Error, since Animal is a supertype of Dog
acceptDogCovariance(new Dog()) // Ok
acceptDogCovariance(new Greyhound()) // Ok since Greyhound is a subtype of Dog
``````

Contravariance :

Contravariance accept `supertype` but doesn't accept `subtype`

``````const acceptDogContravariance = function (value: Contravariance<Dog>) { ... }

acceptDogContravariance(new Animal()) // Ok, since Animal is a supertype of Dog
acceptDogContravariance(new Dog()) // Ok
acceptDogContravariance(new Greyhound()) // Error since Greyhound is a subtype of Dog
``````

Bivariance :

Bivariance accept both, `supertype` & `subtype` !

So now we learn the definitions, but how it's working in Typescript ? Especially for `function`

How Typescript use covariance and contravariance for argument in function ?

A legit question, isn't ?

In typescript, argument types are `bivariant` ! In fact this is not a `correct` behavior, but why ?

Ok ok, we will illustrate this `unsound` case !

``````class Animal {
doAnimalThing(): void {
console.log("I am a Animal!")
}
}

class Dog extends Animal {
doDogThing(): void {
console.log("I am a Dog!")
}
}

class Cat extends Animal {
doCatThing(): void {
console.log("I am a Cat!")
}
}

function makeAnimalAction(animalAction: (animal: Animal) => void) : void {
let cat: Cat = new Cat()
animalAction(cat)
}

function dogAction(dog: Dog) {
dog.doDogThing()
}

makeAnimalAction(dogAction) // TS Error at compilation, since we are trying to use `doDogThing()` to a `Cat`
``````

In one example we can demonstrate that Bivariance for argument type is `unsound`, but don't be sad we can fix this thanks to Typescript 2.6 you just need to use `--strictFunctionTypes` flag in your Ts config.

So `makeAnimalAction` need to be `contravariant` for argument type. Thanks to this we can avoid to make Dog action to a Cat !

``````function makeAnimalAction(animalAction: (animal: Animal) => void) : void {
let cat: Cat = new Cat()
animalAction(cat)
}

function animalAction(animal: Animal) {
animal.doAnimalThing()
}

makeAnimalAction(animalAction) // "I am a Animal!"
``````

How Typescript use covariance and contravariance for returned type in function ?

The returned type of a function in Typescript is `covariant` !

Thank you to read this ..... Ok ok, I will try to demonstrate it !

``````class Animal {}

class Dog extends Animal {
bark(): void {
console.log("Bark")
}
}

class Greyhound extends Dog {}

function makeDogBark(animalAction: (animal: Animal) => Dog) : void {
animalAction(new Animal()).bark()
}

function animalAction(animal: Animal): Animal {
return animal
}

makeDogBark(animalAction) // Error since not all Animal can bark.
``````

Here we need to have a Dog or a `subtype` of Dog in returned type for `makeDogBark` argument. So returned type need to be `covariant`

TL;TR & Conclusion

So in Typescript, argument type need to be `contravariant` and function types need to be `covariant` in their return types.

I hope you like this reading!

🎁 You can get my new book `Underrated skills in javascript, make the difference` for FREE if you follow me on Twitter and MP me 😁

Or get it HERE

☕️ You can SUPPORT MY WORKS 🙏

🏃‍♂️ You can follow me on 👇

👨‍💻 Github: https://github.com/Code-Oz

So `makeAnimalAction` need to be contravariant for argument type.

This statement is confusing because the core concern isn't with the parameter type on `makeAnimalAction` but the argument type being passed - more specifically the argument type on the function type being passed.

• `makeAnimalAction` needs an argument type of `(a: Animal) => void`
• We attempt to pass `dogAction` which has the type `(d: Dog) => void` which should cause an error. Why?
• The full rule is "A function type is a sub-type if its arguments are contravariant and its return type is covariant with reference "the other" function type.
• By that rule `(d: Dog) => void` is not a subtype of `(a: Animal) => void` because `Dog` isn't contravariant to `Animal`.

The returned type of a function in Typescript is covariant.

As a statement this can't stand on its own. The central idea is:

"A function type is a sub-type of another function type if and only if the arguments of the first are contravariant and the return type is covariant with reference to the second function type."

Trying to break it down any further and the original meaning is lost. Covariance and Contravariance are only relevant because we want to determine whether one function type can be considered a sub-type of another.

Covariance/Contravariance also is relevant to overridden methods:

``````class Animal {
doAnimalThing(): void {
console.log('I am a Animal!');
}
}

class Dog extends Animal {
doDogThing(): void {
console.log('I am a Dog!');
}
}

class Cat extends Animal {
doCatThing(): void {
console.log('I am a Cat!');
}
}

class Base {
handle(animal: Animal): void {
animal.doAnimalThing();
}
}

class Derived extends Base {
// Method argument is covariant
// which violates the `Base.handle`
// contract of `(a: Animal) => void
//
// Only contravariant arguments
// and covariant return types
// should be allowed
// on overridden methods
//
// TypeScript does NOT disallow this
// even though this violates the
// Liskov Substitution Principle (LSP)
//
// i.e. `Derived` CANNOT be used anywhere
// where a `Base` is expected because
// `Derived`'s interface is more
// restrictive than `Base`'s.
handle(dog: Dog): void {
dog.doDogThing();
}
}

const dog = new Dog();
const cat = new Cat();

const base = new Base();
const derived = new Derived();
const something: Base = derived; // TypeScript doesn't complain here

base.handle(dog);
base.handle(cat);

derived.handle(dog);
// derived.handle(cat); // This is finally where TypeScript complains.

something.handle(dog);
something.handle(cat); // TypeScript doesn't complain; at runtime: "dog.doDogThing is not a function"
``````

playground

This will likely never be fixed:

Methods are excluded specifically to ensure generic classes and interfaces (such as Array) continue to mostly relate covariantly. The impact of strictly checking methods would be a much bigger breaking change as a large number of generic types would become invariant (even so, we may continue to explore this stricter mode).

Usman Khalil

This is something I've never seen before. I've used classes before and this seems quite a useful pattern to type check