Transcript from the "Generics Scopes & Restraints" Lesson
[00:00:00]
>> The last topic we're going to talk about in TypeScript Fundamentals V3 is generic scopes and constraints. So these are some final details I want to add to the concept of generics, so you can start to make use of them in practical applications. Scopes and constraints have to do with where you can use type parameters depending on where they're defined.
[00:00:28] And minimum requirements you can impose on type parameters such that you can consume them within your function. And that will make a little bit more sense what I mean by that as we get into that topic. So again, I want to begin with a motivating use case, why would we care about scopes and constraints?
[00:00:53] What would be missing if we didn't have these things? So constraints allow us to describe a minimum requirement for a type param. And that is important in order to be able to use, for example, the things within this list beyond what we have seen so far. So we just dealt with this list to dict function while we were passed a callback that's used to generate IDs.
[00:01:23] But let's say we were building something out for a data layer, where every object that we're working with has a property on it called ID. Maybe it's coming from sequel database or something where there's just always this primary key. It's always going to be there, and we always want to use that as the mechanism for storing it in dictionary.
[00:01:49] Well, we don't need this callback anymore, but we do have to define this assumption about what the bare minimum requirement for the kinds of things we expect to be in this list. We need to define that somewhere. So let's dig into this. So the function signature we'd like to arrive at, we'd like to change from something like this to something that kind of looks like this, right?
[00:02:25] So, previously, we kind of had a list of any's here or some other very specific type, but we need everything to at least be an object that has an ID on it. It's a minimum constraint that we need, why? Because as we store items in the dictionary, we're gonna reach into this item, and we're gonna retrieve a property off of it called ID.
[00:02:51] We didn't have this requirement with the first version of this algorithm. Right up here, we were given a function that we could use to get these IDs. Whoever was calling this function, it was their job to tell us what to do. We simply just said, you gave me a callback, I'm going to use this item with the callback.
[00:03:10] I have my ID, I don't need to know how this callback works, you handle that you provide it to me. Well, now we need to be able to reach into this, and grab an ID. So we need everything we receive to have an ID. Here's how this would work.
[00:03:32] This would be the naive approach that would fail, right? If we just tried to say, I have a type paramater T, I get a list of T's, effectively this thing becomes pretty pointless within the function. So if we look at this, so I've got my item here, I've got a T in this callback.
[00:03:53] If I try to get an autocomplete on this thing, there's nothing there for me to use. I cannot safely do anything here, why? Because T could be anything, it could be a list of nulls. I can't safely do anything because there's no minimum requirement for what kinds of Ts I'm willing to accept.
[00:04:18] So we need something better, we need something that allows us to always have that ID there. So what we need to do is describe this constraint. And I'm showing you kind of a one line diff version of the change that we need to make. Because it's useful to understand where the information moves as we start with the non-generic version of our function, and move to the generic version.
[00:04:45] This here, right, this is the minimum base class of minimum constraint we need this stuff to align with. It kind of moves into two places here, one like the array nature of this. We put that over here, but we've got this constraint on that type parameter now. And you might be wondering, are we using extends again, yet again, right, extends.
[00:05:15] What does this have to do with the extends that I already know about? Well, it's kind of related, at least conceptually, in that when you have a subclass that extends from a base class, you're kinda guaranteed to have all of that base class functionality. All of the class fields that were there, all of the methods, and you might have more.
[00:05:36] But at minimum, you are gonna have everything that can be expected of the base class. While here we're saying I can be given any T, but it has to at least meet this base requirement. That's the best I can do at helping you at unifying this concept of extends across everywhere it's used.
[00:06:10] So let's talk about scopes as they have to do with type parameters. In the world of variables, we know that inner scopes can see things that are available in outer scopes. For example in here, I am able to see bowl, and I'm able to see apple, I can see both, right?
[00:06:33] You can you can see from the inside out, but not the outside in type parameters work a very similar way. So here we have a higher order function that's used to create a tuple. So you give it the first thing that goes into the tuple, and then you give it the last thing that you want to see in the tuple, just a tuple of size two, right?
[00:07:00] So ultimately, what's gonna happen, if we look at the usage here, we've got a tupleCreator. And that returns a function, and then we finish the tuple by giving that an argument. So everything's gonna start with three, but then it'll finish with null or finish with this array of numbers.
[00:07:22] And I just wanna make it clear that here you can see we're allowed, just as in this case above where down here we can access both fruit and, sorry, both apple and bowl. Here, we can access both T and S because inner scopes can see outer scopes. Just be careful you don't make things too complicated with this.
[00:07:46] But a more pragmatic example might be you have a type parameter that's on a class. And then you have a method that has another type parameter of some sort. So finally, the last thing I want to leave you with here are some best practices around using generics. So first, make sure you remember that the point of these type parameters and of generic types is to relate multiple things, right?
[00:08:24] In the case of our list to dictionary example, we're saying the list I'm given is related to the callback. They're of the same type, they both involve a T, right? And then I returned something that has a T in it, so they're all linked. If you're not using a type parameter more than once, you can end up casting inadvertently, basically, forcing TypeScript to regard something you have as a different type.
[00:08:58] So let's look at this, let's look at what's going on here. We've got a function here that says, return as string. Shouldn't be called returning a string, but return as something. So we give it an argument which isn't any and it returns a T. But look, we can give it a window, and we're gonna be forced to specify a type parameter explicitly here because nothing can be inferred here.
[00:09:27] We're not like accepting a T, we're just returning a T. But effectively, what ends up happening here is Windows passing straight through. But we've changed its type to a number, and this is should be obvious why this is dangerous, right? It's typecasting using a different mechanism. And typecasting is typically done with this as keyword where we're saying, look, just I know this is a window, but I want you to regard it as something else.
[00:09:57] So when doing a code review, you might be scanning through to see if someone's written as, but this is typecasting too. A single use of a type param, right, it's only used here. This will have the same effect, it's just a harder problem to catch in a code review.
[00:10:17] See two or more times, that's where you want to use type params. This here, we call this convenience casting, and if you're gonna cast, cast in a way, that is obvious for code reviewers. It may be what you want to do, but you want to have that discussion, and you want it to be clear that you're going to change the type of this thing.
[00:10:47] So you want to define your type parameters as simply as possible, and in a way that allows you to take advantage of all of the static analysis and the inference that TypeScript has to offer. So in this case, notice that, sorry, we have two options. We can either say T is a type, and that type extends from an array of HasIds, versus down here we could say T extends HasId.
[00:11:29] And the benefit here is basically your types are gonna get a lot dirtier, and depending see the tool tip here, it's sort of being inferred differently. I mean, it's kind of strange, right? The list is a T, T extends an array of HasIds, let's see what the return type is here.
[00:11:54] Look at that, look at what the return type is, it's a has ID or an undefined, whereas the return type here, it's a T or undefined. We've actually lost a type parameter here, isn't that interesting? And that's because when the return type is analyzed, we don't know what T is, right?
[00:12:14] This is a list, all we know is it's at least a HasId, but the build time analysis can't figure out what the return type should be until it's actually called. And that is a problem that is why we have lost the T in the return type here. So you don't need to understand too much about why this is bad, but the best practice is to sort of push your type parameters down to the lowest level, the simplest way of describing them as you can.
[00:12:50] And then in your argument list, of course, you don't have to just take T straight up as an argument. You could take an array of Ts, a promise that resolves to a T, whatever you like. And there you can see the type checking will do its job more easily and more successfully.
[00:13:07] And then finally, the last thing I want to leave you with is don't make things generic unless there's real value in doing so premature abstraction is bad. It makes it hard for you to understand your own code, and for others to understand your code. So just make sure that you start out with simple things, and don't get into generic hell, where everything has 15 type params, and just becomes hard to even think about what's going on.
[00:13:38] These type parameters should have a purpose, you should have a small number of them, and just don't let the complexity get out of control.