Dmitri Pavlutin
I help developers understand JavaScript and React

Covariance and Contravariance in TypeScript

Posted October 14, 2021

Learning covariance and contravariance in TypeScript could be tricky (I know from my experience!), but knowing them is a great addition to understanding types and subtyping.

In this post, you’ll read an accessible explanation of covariance and contravariance concepts.

1. Subtyping

Thanks to inheritance, in TypeScript a base type can be extended by another type named subtype.

For example, let’s define a base class User, then extend that class by an Admin class:

ts
class User {
userName: string;
 
constructor(userName: string) {
this.userName = userName;
}
}
 
class Admin extends User {
isSuperAdmin: boolean;
 
constructor(userName: string, isSuperAdmin: boolean) {
super(userName);
this.isSuperAdmin = isSuperAdmin;
}
}

Since Admin extends User (Admin extends User), you could say that Admin is a subtype of base class User.

Subtyping is possible not only in classes but also in other types. For example, the literal string type 'Hello' is a subtype of string, or the literal number type 42 is a subtype of number.

Now let’s introduce the symbol A <: B — meaning “A is a subtype of B”. Because Admin is a subtype of User, now you could write shorter:

Admin <: User

Additionally, I’m going to use a helper type IsSubtype<S, P>, which evaluates to true if S if a subtype of P, and false otherwise:

ts
type IsSubtype<S, P> = S extends P ? true : false;

Since Admin is a subtype of User, as expected, IsSubtype<Admin, User> is true:

ts
type T1 = IsSubtype<Admin, User>;
type T1 = true

2. Covariance

Let’s think about some asynchronous code that fetches User and Admin instances. Thus, you have to work with promises of User and Admin.

Having Admin <: User, does it mean that Promise<Admin> <: Promise<User> holds as well? In other words, is Promise<Admin> a subtype of Promise<User>?

Let’s see what TypeScript is saying:

ts
type T21 = IsSubtype<Promise<Admin>, Promise<User>>
type T21 = true

TypeScript has showed that indeed Promise<Admin> <: Promise<User> holds true as result of Admin <: User. Saying it formal, Promise type is covariant.

Covariance

Here’s a definition of covariance:

A type T is covariant if having S <: P, then T<S> <: T<P>.

The covariance of a type is intuitive. If Admin is a subtype of User, then you can expect Promise<Admin> to be a subtype of Promise<User>.

Covariance holds for many types in TypeScript, for example:

ts
// Capitalize<T>
type T22 = IsSubtype<'Hello', string>;
type T22 = true
type T23 = IsSubtype<Capitalize<'Hello'>, Capitalize<string>>;
type T23 = true

3. Contravariance

Now let’s consider the following generic type:

ts
type Func<Param> = (param: Param) => void;

Func<Param> creates function types with one parameter of type Param.

Having Admin <: User, which of the expressions is true: Func<Admin> <: Func<User>, or Func<User> <: Func<Admin>.

In other words, is Func<Admin> a subtype of Func<User>, or vice-versa: Func<User> is a subtype of Func<Admin>?

Let’s take a try:

ts
type T3 = IsSubtype<Func<Admin>, Func<User>>
type T3 = false
type T4 = IsSubtype<Func<User>, Func<Admin>>
type T4 = true

As the example above shows, Func<Admin> <: Func<User> is false.

On the contrary, Func<User> <: Func<Admin> holds true — meaning that Func<User> is a subtype of Func<Admin> . The subtyping direction has flipped compared to the original types Admin <: User.

Such behavior of Func type makes it contravariant. In other words, function types are contravariant in regards to their parameter types.

Contravariance

A type T is contravariant if having S <: P, then T<P> <: T<S>.

3.1 The idea of contravariance

The dry theory above is a bit difficult to understand, so let’s find the intuitive sense behind contravariance.

Let’s define 2 functions that log the information stored in an Admin and User types of instances. logAdmin() logs Admin instances, while logUser() logs User instances information to the console.

ts
const logAdmin: Func<Admin> = (admin: Admin): void => {
console.log(`Name: ${admin.userName}`);
console.log(`Is super admin: ${admin.isSuperAdmin.toString()}`);
}
 
const logUser: Func<User> = (user: User): void => {
console.log(`Name: ${user.userName}`);
}

Now let’s try to assign logUser() function to a variable of type Func<Admin>, would it work?

ts
const logger: Func<Admin> = logUser; // OK

Yes, you can! Thanks to contravariance.

The variable logger is of the base type Func<Admin>, so you can assign to it logUser function of type Func<User>.

Now let’s try the other way around: assign logAdmin() function to a variable of type Func<User>:

ts
const logger: Func<User> = logAdmin;
Type 'Func<Admin>' is not assignable to type 'Func<User>'. Property 'isSuperAdmin' is missing in type 'User' but required in type 'Admin'.2322Type 'Func<Admin>' is not assignable to type 'Func<User>'. Property 'isSuperAdmin' is missing in type 'User' but required in type 'Admin'.

Nope, you can’t! Again, thanks to the contravariance of functions in regards to parameter types.

Why…?

logger variable is of type Func<User> — the subtype. And you cannot assign a variable of base type (logAdmin is Func<Admin>) to a subtype.

As an experiment, let’s disable type checking on the assignment by using type assertion to any. Then let’s see what would happen during runtime:

ts
const logger: Func<User> = logAdmin as any;
 
const user = new User('user1');
logger(user);
"TypeError: Cannot read properties of undefined (reading 'toString')"

Try the demo.

A runtime error is thrown because logger(user), where logger is logAdmin, cannot log the instance data because isSuperAdmin property doesn’t exist in the class User.

Contravariance prevents such errors on functions subtyping.

4. Conclusion

The type T is covariant if having 2 types S <: P, then T<S> <: T<P> (the subtyping direction is maintained). An example of covariant type is the Promise<T>.

But if T<P> <: T<S> (the subtyping is flipper), then T is contravariant. The function type is contravariant by the parameter types.

Challenge: What other covariant or contravariant types do you know?

Like the post? Please share!

Quality posts into your inbox

I regularly publish posts containing:

  • Important JavaScript concepts explained in simple words
  • Overview of new JavaScript features
  • How to use TypeScript and typing
  • Software design and good coding practices

Subscribe to my newsletter to get them right into your inbox.

Join 4381 other subscribers.

About Dmitri Pavlutin

Software developer, tech writer and coach. My daily routine consists of (but not limited to) drinking coffee, coding, writing, coaching, overcoming boredom 😉.
Email addressTwitter profileFacebook pageLinkedIn profile