Pawk3k

Demystifying Type Assignability in TypeScript: Covariance and Contravariance Explained

Published on

Types relationship

The most common question in Typescript as a type system is "How do those types relate to each other?" or "Are those types assignable/interchangeable?".

And when you are not certain about the relationship between two types, you can always ask the Typescript compiler to help you with that.

ts
type Answer = string extends 'abc' ? true : false
type Answer = false

You might be wondering why this article is even needed. Typescript is a structural type system, meaning that if two types have the same structure, they are the same type.

Covariance >

Yes, it is, but what if we have a variable of the type Person and try to assign the variable with more properties than the type has?

ts
type Person = {
name: string
age: number
}
 
type Professional = {
name: string
age: number
profession: string
}
 
const professional: Professional = {
name: 'John',
age: 30,
profession: 'warehouse worker',
}
 
const person: Person = professional

So what happened here was we are trying to assign a professional to the person, and this is OK behaviour because the professional type satisfies all the requirements of the person type. Hence, the typescript knows it has nothing to worry about. So, the Professional is more than > just a Person, meaning that the Professional is covariant to the Person.

ts
type Answer = Professional extends Person ? true : false
type Answer = true

Contravariance <

So now we know - we need to satisfy the contract, and the typescript will not complain.

Let's make a function type:

ts
type toString = (a: number) => string
 
const myToStringImplementation: toString = (a: number) => a.toString()

No errors, no cry

Now, let's try break the contract:

ts
type toString = (a: number) => string
 
function someImplementation(a: number) {
return a
}
const myToStringImplementation: toString = someImplementation
Type '(a: number) => number' is not assignable to type 'toString'. Type 'number' is not assignable to type 'string'.2322Type '(a: number) => number' is not assignable to type 'toString'. Type 'number' is not assignable to type 'string'.

So what happened here? We looked at the function signature and found that the return type should be a string, but if we take a number and return a number, we probably made an error unintentionally, and Typescript is happy to help us here.

ts
someImplementation
function someImplementation(a: number): number
type Answer = number extends string ? true : false
type Answer = false

Now, let's try to break the contract in another way:

ts
type toString = (a: number) => string
 
function someImplementation() {
return 'meaning of life'
}
 
const myToStringImplementation: toString = someImplementation

So here we are, not getting any errors, and typescript is happy with our implementation. Why is that? Is typescript a make fun of us? In the type above, we specify that the function should take a number and return a string, but someImplementation doesn't accept any arguments.

Here, contravariance comes and tries to save the day.

The direction of the assignment is reversed for the function parameters. So here is where < logic comes into play. Let's think about the type of someImplementation. it equals ()=>string, and that means (params: never)=>string So the next question: does never is an assignable to a number?

ts
type Answer = never extends number ? true : false
type Answer = true

So it does, and if we think about it, it makes sense from the type system perspective. The javascript function can be called with any number of parameters. If we are specifying a function that is not taking parameters, then in the scope of the function, we are not using them, which means that even if we got some parameters, we would not operate on them. The context of the function is good enough to produce a value or do some side effects. And of course, we can access the arguments, but this is not typesafe or the point of this article.

Conclusion

So, in our language, we have two directions of the assignability

Covariant > - For all the things except the function parameters

In case of covariance more is better, and less is worse also means tha to satisfy the contract we at least should have all properties required by the contract.

Contravariant < - for the function parameters

In case of contravariance less is better, and more is worse that means we cannot assign function parameters of wider type to the function parameters of narrower type.

Links

stack

stack

illustrated guide

type-leveltypescript