DEV Community

Cover image for How I understand Covariance & Contravariance in typescript
Code Oz
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 {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

๐ŸŽ MY NEWSLETTER

โ˜•๏ธ You can SUPPORT MY WORKS ๐Ÿ™

๐Ÿƒโ€โ™‚๏ธ You can follow me on ๐Ÿ‘‡

๐Ÿ•Š Twitter : https://twitter.com/code__oz

๐Ÿ‘จโ€๐Ÿ’ป Github: https://github.com/Code-Oz

And you can mark ๐Ÿ”– this article!

I use https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance in order to understand and explain this article

Oldest comments (3)

Collapse
 
monfernape profile image
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

Collapse
 
peerreynders profile image
Info Comment hidden by post author - thread only accessible via permalink

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"
Enter fullscreen mode Exit fullscreen mode

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).

Collapse
 
ahmad-ali14 profile image
Ahmad Ali

following...

Some comments have been hidden by the post's author - find out more