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
subtypeof Animal, and Animal is thesupertypeof Dog.Dog is the
supertypeof Greyhound and Greyhound is asubtypeof 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 👇
🕊 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
Top comments (3)
This statement is confusing because the core concern isn't with the parameter type on
makeAnimalActionbut the argument type being passed - more specifically the argument type on the function type being passed.makeAnimalActionneeds an argument type of(a: Animal) => voiddogActionwhich has the type(d: Dog) => voidwhich should cause an error. Why?(d: Dog) => voidis not a subtype of(a: Animal) => voidbecauseDogisn't contravariant toAnimal.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:
playground
This will likely never be fixed:
This is something I've never seen before. I've used classes before and this seems quite a useful pattern to type check
following...
Some comments have been hidden by the post's author - find out more