Covariance and Contravariance in Typescript
Keep in mind that we’re going to use the following principle throughout the article: we can always substitute a type with its subtype.
Covariance
Suppose we have these simple types:
We know that BlogWithImage
is a subtype of Blog
. Passing a BlogWithImage
to a function that expects Blog
should not make the compiler bark.
And the following example should not typecheck:
So from these instances, we can draw a conclusion that BlogWithImage
is indeed a subtype of Blog
as we can use it as a Blog
substitute.
Contravariance
However, when it comes to functions, things are a bit different. Suppose we have fetchBlog
function which takes a callback:
Providing printTitleImg
will not typecheck and will throw at runtime cause the img
field will not be present upon fetching the blog (well, not necessarily. But imagine if it was a nested field). The fact that we can’t substitute (b: Blog) => void
with (b: BlogWithImage) => void
means that (b: BlogWithImage) => void
is NOT a subtype of (b: Blog) => void
What about the other way around?
Passing printTitle
typechecks and we won’t encounter any runtime error since it only consumes title
from the BlogWithImage
object and just doesn’t care about the rest. So in this case (blog: BlogWithImage) => void
is substitutable with (blog: Blog) => void
, thus we can say that (blog: Blog) => void
IS a subtype of (blog: BlogWithImage) => void
.
When it comes to function arguments, the more general type will always be the subtype of the more specific one. This is the opposite (contra!) of what we usually encounter 🙂
Inferring in Contravariant Position
Suppose we have:
What will be the resulting type of WhatAmI
?
We know that if there’s an expression of A extends B
, it’s A
that is the subtype and B
that is the base type. So in this case, BlogFn
is the subtype, and (b: infer T) => void
is the base type.
And we know that:
{ title: string }
is more general than{ title: string, img: string }
{ img: string }
is more general than{ title: string, img: string }
Remember, when it comes to function arguments, the more general type will always be the subtype of the more specific one. Thus it follows that:
(b: { title: string }) => void
is the subtype of (extends)(b: { title: string, img: string }) => void
(b: { img: string }) => void
is the subtype of (extends)(b: { title: string, img: string }) => void
From these premises, it makes total sense if infer T
should give you { title: string, img: string }
as it is the very minimum compatible type for each of the BlogFn
constituents: it’s now capable to handle both { title }
and { img }
. And indeed, the resulting type of WhatAmI
is { title: string, img: string }
🙂.
If you look closely, it seems like the compiler converts a union type into an intersection type when inferring in contravariant position.
Interesting! Why don’t we just create a utility type that converts union types into intersection types?! 💡
The U extends unknown
expression makes sure that we distribute/iterate over the union U
. As stated in the documentation, “When conditional types act on a generic type, they become distributive when given a union type.” And the rest is similar to what we have just discussed.
I hope you find this article useful. Stay well!