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 thesupertype
of Dog.Dog is the
supertype
of Greyhound and Greyhound is asubtype
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
π 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
Top comments (3)
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
dogAction
which has the type(d: Dog) => void
which should cause an error. Why?(d: Dog) => void
is not a subtype of(a: Animal) => void
becauseDog
isn'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