Intermediate TypeScript, v2

Variance over type Params

Intermediate TypeScript, v2

Check out a free preview of the full Intermediate TypeScript, v2 course

The "Variance over type Params" Lesson is part of the full, Intermediate TypeScript, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Mike discusses variance over type parameters using an example of a snack factory. They explain the concepts of covariance and contravariance and how they relate to subtyping relationships between different types. The instructor also demonstrates how to use the "out" and "in" keywords in TypeScript to describe covariance and contravariance respectively.


Transcript from the "Variance over type Params" Lesson

>> Variants over type parameters, out, you will understand what variants means by the end of this but as before, we're gonna use an example-based approach where you can see right in front of your eyes how something behaves. We'll give it a name at the end. And let's get started.

So, I have a situation where I run a snack factory. This is our example. I run a snack factory. I make different kinds of snacks for now. It's just cookies and pretzels. So I have a snack base class, a Boolean that indicates whether something is pet friendly or not.

On all snacks it has that. Pretzels are pet friendly based on whether they're salted or not. And then cookies, because these happen to be chocolate cookies, they're never pet friendly. Dogs can't eat chocolate. Maybe some pets can, but we can't say it's pet friendly in general. So, okay, the important thing to remember here is a cookie is a subtype of snack.

All cookies are snacks. Not all snacks are cookies. Specifically, pretzels are not cookies. So in my factory, I want to model machines that produce cookies. So I make an interface that kinda describes this called producer. Producer is generic over the thing that it produces, which is represented here by type T.

We can create a cookie producer. Produce makes a new cookie. Always dark chocolate it happens. A snack producer while this machine produces different types of snacks, I have here it does a coin flip. And it could produce a milk chocolate cookie if it's heads, or it could produce a pretzel if it's tails.

I want you to imagine this as like, look, producing a snack we don't necessarily know what kind of snack is gonna come out, we know it's going to be a snack. So, Let's try assignment in both directions. If we need a snack producer, we need something that produces snacks of some sort.

Will it be okay if we use the cookie machine? Yep, I see some nods in class. So yeah, cookies are snacks. If I need a machine that makes snacks and I have a cookie machine, that's fine. But if I need something that only produces cookies, will a snack producer do?

And the answer here is no. Some pretzels are gonna come out of that thing. If I need specifically a cookie machine, Those pretzels are kind of unwanted. What we're observing here is called covariance. And I'm gonna read these rows from left to right. A cookie is a subtype of snack.

A cookie producer is a subtype of snack producer. The fact that both of these arrows are pointing in the same direction. That means that producer is covariant over type T. The type, this type is defined in such a way that if we have like types A and B, and A is a subtype of B, producer A is a subtype of producer B.

The subtype directions are the same for both the things that are filling in the type param and the producer that is using the type param. TypeScript gives us a way to describe this. We can add a keyword before the type param that is out. It's easy to remember that the out keyword pertains to producer because the produce function returns the type parameter.

The type parameter comes out of produce. Let's look at a different situation. I have a different machine now. A packaging machine. And I define a type to describe this. This is a machine that takes some sort of, collection of snacks, and it puts them into boxes or bags or whatever needs to happen in order for us to be ready to ship them out.

We can make a cookie packager. So we're passing in cookie as a type param, and we see that we're taking cookie as an argument here. And we can make a snack packager. And if we look at this the way this is implemented, we could see that this machine it's like a cookie packager plus plus.

It'll package cookies. But if the item it's given is a pretzel, it knows how to do that too, potentially if there are other snacks in the future, maybe it knows how to do that as well. It's like imagine this is a cookie packaging machine with like a pretzel packaging attachment to it.

It can do everything a cookie packager does and more. Let's check for type equivalence. Well, if we need a cookie packager, yeah, the snack packager will do. I mean it's packaging cookies right up here on line 62. But if we need something that packages a wide range of snacks.

You can't just put a machine in there that only handles the cookies. You won't be handling the complete set of possibilities of what a snack could be. Let's bring that table back. So, reading in the direction of the arrows, I would say a cookie is a subtype of snack.

A snack packager is a subtype of a cookie packager. Meaning snack packages can stand in for cookie packages, but they're specialized, they have extra stuff in them. Similar to up here but in the other direction. We were saying like, look, if you need a snack producing machine, a cookie machine can spit out snacks, that's no problem.

Cookies are snacks, you need me to give you something that matches the contract described by the snack interface. I'll chuck cookies at you all day, it's fine. So when these arrows are in opposite directions, we say that this type packager is contrarian over its type parameter T. For two types that you give it.

If a is a subtype of b, and then you look at the packagers, they flipped directions. A is a subtype of b. But packager b is a subtype of packager a, contravariance. TypeScript gives us a way to describe this using an n keyword. It's kinda easy to remember this one because the item goes in.

This will contravariant behavior often happens when you accept your type parameter as an argument, covariate behavior often happens when you return your type parameter, in and out.
>> Do you find yourself using the variance helpers very often?
>> These are so new that I really haven't had a chance to use them much.

Where I have added them is a centrally, like I have a repo that has JSON types. Which if you took TypeScript fundamentals, that's literally the contents of the repo, and it's used by a lot of things. I've added this to that because it's a recursive type, and for deeply nested JSON, you can end up running through that recursion many, many times.

But I would strictly not recommend putting this everywhere throughout your code. It's just if you're trying to have something that you're preserving the covariant or contravariant nature of it. You could put this up as sort of a guardrail where at least you know when you've crossed into that territory.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now