Intermediate TypeScript, v2

Generic Constraints

Intermediate TypeScript, v2

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

The "Generic Constraints" 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 reviews a TypeScript Fundamentals exercise that involved converting an array of objects to a dictionary representation and demonstrates how to implement this conversion without using generics. The use of type parameter constraints to regain flexibility while still maintaining a minimum requirement on the type parameter is also discussed in this segment.


Transcript from the "Generic Constraints" Lesson

>> The next topic we're gonna take on, represent us picking up where we left off in TypeScript Fundamentals V4. We were talking about generics at the end of that course, and now we're gonna talk about generic scopes and constraints. Generic constraints are the ability to specify a minimum requirement on a type parameter.

And I'm gonna begin with an example that illustrates, the importance of having something like this. So in the exercise we completed in the generic section of TypeScript Fundamentals V4, it involved us taking an array of objects that have some sort of ID on them, in this case, customerId.

And we converted them to kind of like a equivalent dictionary representation of the same data. And you can see here, we're just using the customerId as the key in our dictionary and we can look up a customer by ID now without having to iterate through a list. So the working code we ended up with at the end of that exercise was this and just to refresh everybody's memory in case it's been a little bit since you took the course.

And we've talked about a lot of things so far, so we'll just, well, I'll catch up here. We ended up with a function called listToDict that takes in a type parameter. And that T represents a couple different things, or it's used in a couple different ways. But here's what our T is.

It effectively represents one of these members of the array, or one of these values in our dictionary. It's the same shape, right? It's three properties and there's an object. Now, we ended up with a very flexible implementation that can handle a wide range of different shapes. But that's where the shape manifests, right?

It's the type of an array member. So we've got this type parameter T, here's the list we take in as input, and it's an array of Ts. So this is where the inference happens, right? As soon as we say, here's the array that I want to convert to a dictionary.

listToDict will then understand what T is and it will help us provide this ID generation function. And it'll know what the return type is supposed to be cuz it can infer T based on the list that we give it. So we're gonna enter inside of the function we create an empty dictionary of the correct type, so it can have any key that is a string, and it can have a value of type T.

We iterate over the list, we call this ID generation function, which we pass it an item, it gives us an ID. Here's that function. It's a function just for obtaining an item's ID. We leave it to the caller to provide that for us so that we can be maximally flexible, and then we return the dictionary.

So this is the most complicated thing that's going on, but hopefully, we can understand how this works. It's iterating over an array and it's transforming it into a dictionary or a hash. All those words mean kinda the same thing. Okay, so let's think about how we might implement this.

Let's add an additional convention to the solution that we made for the TypeScript fundamentals problem. So here we have customer ID and an ID generation function. But let's say we wanted to drop the ID generation function, and just operate by some convention where if we have properties with ID, we'll just pick that up.

We just know that there's an ID that's going to be present, and we can use that, right? That lets us simplify this. We can get rid of this, and look at how much easier this function is to understand, right? You get a list of T's, you get a dictionary of T's fed back to you.

It's less abstract and maybe for our use case, everything has an ID property and this will do just fine. So let's look at how we might implement this without using generics. Well, first, we could create a type alias or an interface, it doesn't really matter, called HasId. This is, represents anything that has a property called ID whose value is a string.

We're going to want an interface that represents a dictionary. So this is a dictionary that's generic over type T which is the value type that's in the dictionary, and we've removed generics now. So this function gets a little bit simpler and let me comment out this one above just so we don't collide.

So we take in a list of objects that are HasId, that make sure that we have an ID on each object. We can iterate over each of the items because there HasId, well, we know it's there. We don't need to call this function to generate IDs anymore. We just know the properties there because it's sort of like a minimum requirement we have placed on list, and we have that guarantee now.

And then we return the dictionary and it seems like it works. So the problem here is we have lost, if we do listToDict, Let's not do phone list because those don't have IDs. So we'll say a and then, Something like that. Let me create these in a variable here, so I don't get excess property checking yelling at me.

Just create a bunch of these. All right, so we've got an example of things that have IDs on them, and we're gonna call listToDict, pass in the testArr. What's our test result? And if we look at test result, Look at that, only the id property is available. And of course, that's because this is what we're returning.

So we've stated a requirement on our input but that requirement is kind of, all we know about this is that they have the property ID, we can't create a more specific result here. We've lost the flexibility that our generics, or type parameter gave us. The point of the type parameter was we could be flexible, while still retaining type information and letting it pass straight through our function.

So let's see if we can bring our type parameter back into the function. So this is the new function head or the beginning of the declaration. So I'm gonna drop it in place. And just so you can look at them side-by-side, what we're doing here is we're replacing HasId with T.

And instead of a dictionary of HasId, it's a dictionary over type T. So if we use that, Well, we'd have to change this to T as well. So if we look at our test result, that looks a lot better, right? Color is there. If we did testResult.a., there's color.

The type information is passing through our function again, but look at this, we can't because T could be anything. T could be literally anything. So what we need here is to describe a constraint. We've looked at two options here. In this option, we had some guarantee, like a minimum requirement, for what we could accept in our list.

It has to be objects containing an ID property. And here, we have flexibility, but we lost that minimum requirement. A type parameter constraint lets us have both. It lets us kind of get the best of both worlds here. Let me comment this out, so it doesn't screw up syntax highlighting.

And we already put that in. So what we're gonna need to do here is say, T extends HasId. There's our type parameter constraint. T extends HasId. There's our id. So that internally, these look like HasId objects. In fact, look at that. It's being regarded as a HasId. Look at the tooltip.

But the distinction here is, we're still using a type parameter here. Anything that goes beyond satisfying the HasId interface, like this color field, it's preserved and it flows through this function.

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