TypeScript Fundamentals, v3 Generics
Transcript from the "Generics" Lesson
>> We have now reached the culmination of TypeScript fundamentals v3, and that is generics. So generics are a way of creating types that are expressed in terms of other types. And the benefit of doing this is it allows for a greater opportunity to reuse code across your app.
[00:00:23] And that'll become clear as we get deeper into this topic. Because this is an abstract concept, I want to begin by leading us up to why we should care about this? What's the scenario we can see where we're missing something from TypeScript? And that will help us understand why should we even care about this thing?
[00:00:46] So let's go back to an example that we've used before in this course, and that is a dictionary, right? We have this dictionary called phones and under an arbitrary key, we can store a three part phone number. And we can see that all of these properties look very, very similar.
[00:01:06] So sometimes it's convenient to organize our data in dictionaries and sometimes it's convenient to organize data in arrays. So it would be nice if we had some sort of utility that let us transform data that was in one type of collection into the other. So let's deal with this as our starting point for our data.
[00:01:29] It's a object that has three parts for a phone number. It's kind of customerId now, so it's just a ten digit phone number. But we've got a customerId, areaCode, phone number, and we have a list of them. What we want in the end is something that looks like this, where effectively we have this key value data structure, a dictionary, where each member of what was in the array, it's stored under a particular key.
[00:02:00] In this case, it's the customerId. Now we're gonna start by implementing a very specific solution that works for exactly this use case. And then as a second step, we're gonna see if we can make it more generalized and kinda get a feel for where we're at at that point.
[00:02:19] So we wanna go from this to this. So we will need one thing along the way and that's the the mechanism for producing a key that we use for storage of each object on the dictionary. And in order to make sure that we're on our way to a generalized solution, we're going to want the color of this function to provide us with the mechanism of obtaining that key.
[00:02:46] So part of what we're gonna ask for here is a little callback function, sort of a higher order function approach to solving this problem. So our function signature could look like this, we've got this PhoneInfo type and then a function that we can use to transform a list of these PhoneInfo things to a dictionary of the PhoneInfo things.
[00:03:16] Here's that little id callback that I mentioned earlier, where in addition to being provided the list, we're also provided with this function that can be used given a PhoneInfo, obtain a string. And we will use that string as our key. This is throwing an error right now for obvious reasons like we're not returning anything.
[00:03:40] We said we'd return a dictionary, currently we're not returning anything at all, quite understandable. So implementing the function itself is not really the point of what we're talking about here, it is pretty simple. We can start by defining the dictionary and we'll start with it empty. We'll iterate through each member of the list will generate a key using the function that were provided.
[00:04:07] And then we'll use that key to store that member of the array in the dictionary and then ultimately at the end, we return the dictionary. So that's the whole algorithm, loop, find the key, shove it on the dictionary, and then return the dictionary at the end, not too bad.
[00:04:28] So we can actually run this in the TypeScript playground and convince ourselves that this works. So the way you do that is you go click on Logs. As in Console Logs, that's where this will show up, and then hit Run. So here we go, this is our input.
[00:04:48] And then down here is where we're doing the log and we're seeing each of these objects stored in the dictionary under a key. So we can see that it appears to work, great. So let's attempt to make this a little bit more general. And the approach we're gonna take is find every reference to this PhoneInfo type and let's replace it with the most general type that we have, an any.
[00:05:17] So we used to have an array of PhoneInfos here, so it's an array of any. Our idGen callback used to take a PhoneInfo as an argument, now it takes in any. And then we used to return a dictionary of PhoneInfos, but we return a dictionary of any's now.
[00:05:36] Nothing else in this function has changed, none of this has changed, it's all the same. So really it's just about the any's that appeared in the function signature. Now if we wanna try this out, we can, now I'm going to comment this last line out because it's really just to illustrate a point.
[00:05:57] But if we run this, go to Logs and Run, we can see that now we're able to use this on a different type of array, right? These are not PhoneInfo objects, these are just objects that have a name property. And I have my little callback that describes what am I using as my key?
[00:06:18] How am I storing these on the dictionary? So we obviously get the correct result at runtime, but the problem is we've lost all of our useful type information. Everything that comes out is in any, so in making this more flexible, we've had to relax our types, so much, that we don't get any of that validation.
[00:07:19] So the first thing we'll need to do is define a type parameter. You can think of type parameters as arguments for types. They're kind of function arguments, but there are four types. Just like functions can return different values given different arguments passed to them, type parameters can influence what generic types end up being.
[00:07:45] So we're gonna go ahead and do a couple things here. This is gonna be our new function signature, so there are some things that have been added here. And we're gonna talk about exactly what each of those pieces of syntax mean, just so we slow way down and we understand what's going on.
[00:08:07] So first, we have this angle bracket t thing to the right of the name of the function, right? It's this thing right here, so what is this, it is the type parameter list with one type parameter in it. You can think of it like the equivalent to the round parentheses for a function argument list.
[00:08:32] So if we had many type parameters here, it might be t,x,y, something like that. The convention is like a common convention you will see but it's not a limitation by any means. It's to use capital letters, often the letter T is used. And this you can trace that back to the use of a similar feature set in C++, where they would call these template classes.
[00:09:03] It's the same thing, you could remember T for type or T for template, so that's what this is, it basically says there exists a type parameter t. The second thing, right instead of receiving a list of PhoneInfos or an array of PhoneInfos, we're receiving a list of T's.
[00:09:27] So what's that all about? Well, what's going to happen is on a per invocation basis, you might end up with a different type T. So if we were passing this, a list of PhoneInfos, T would be PhoneInfo. If we were to pass it a list of strings, T would be string.
[00:09:51] So what we're gonna see is that every time we use this function, TypeScript's going to use the type of the array. This first thing that the function receives, it'll use that to figure out what T should be. So it's gonna infer what T is on a per usage basis depending on the array we pass in.
[00:10:20] This is a much simpler example that kind of boil things down to the absolute most small atom that we can talk about here. We could define a function here that takes an argument and just wraps it in an array, like it's a tupple of length 1. And whatever we give it, it's gonna return a Tupple of 1 with that thing in it, that's all it's doing.
[00:10:52] So let's look at the return types of this function. Up here you can see it's like wrapInArray<T>, right, arg is T, and it returns this. While we pass it a number, look, the T turned to a number. We received an argument of type number, we return a tupple with a number in it.
[00:11:13] Here's the date, here's the date, and here's the date. I could do a RegExp, we could do whatever we want. This type, it's almost like it's morphing to accommodate our needs depending on the argument we pass to it. So it's like auto detecting what T should be, and returning the right thing to us as a result.
[00:11:38] So let's go back to our example again, our listToDict example. So we talked about this type parameter list, right? Just saying T exists, we talked about how we're saying, I'm going to receive a list of arguments. I'm gonna receive an argument and it's defined in terms of T, that's gonna help TypeScript infer what we should get for T.
[00:12:01] So now we have this line here, that's our id generating callback and we're using T here as well. So this means a couple of things. First, that we're going to get the use of type checking within this callback. And the second is we're going to effectively ensure that the type of thing or callback is designed to work with is the same thing that the array has within it.
[00:12:31] So if we had something like this, here's our array, it's an array of dates, this arg is a date, we'd better be treating it as a date. We can't give it a function that's designed to generate ideas for anything else, its gotta be happy with working on dates.
[00:12:59] The last thing to look at is the return type. So we can see that here, it's just a dictionary of T's. And similar to how in our tiny, tiny example, right, we had a return type that looked like this. And we can see that the type of what comes out of the function changes depending on what we pass in.
[00:13:23] Similarly down here, we're gonna get a different kind of dictionary out. So let's put all of this together and take a look at our original example. So all we've done is we've brought the algorithm back in, it's a very simple little looping process here. And let's look at our tool tips as we attempt to use this.
[00:13:47] So here we're getting a dictionary out and we're passing in just objects with a name property. And look at the dictionary we get out, it's got an index signature, where each value that's found in the dictionary, it's just an object with a name. Let's look at a different example, here's our phoneList, right, a array of customer ids and phone numbers.
[00:14:16] Well, you can see that the type has sort of adapted here. It's inferred what T should be, and it's gonna return the right kind of dictionary. So now, unlike our example where we used any, and where we lost all type information as we pass through this function, we tried to make it flexible enough that it would work for any list to dictionary transformation.
[00:14:45] Unlike the any based approach, we're actually maintaining all of our type information. It kind of passes straight through. So we can run this in the TypeScript playground and we can convince ourselves that it works. So up here, this is our first dictionary, that is this right here and then down here this is our second dictionary.
[00:15:14] What we're doing here, I'm just gonna look back at the function signature here. What we're doing here is we're saying on a per invocation basis, we will figure out what T is and for that invocation T serves as a way of defining a relationship between things. Meaning, the dictionary I will give you is related to the list you gave me.
[00:15:42] The callback you'd better give me must be related in terms of type to what's in your list. It serves as sort of a linkage between all of these things and it allows us to remain flexible without basically widening our type and making it so general, so nonspecific that we lose our safety.