Transcript from the "Mapped Types" Lesson
>> We're about to see all of the concepts we've talked about today come together through something called mapped types. This is the most powerful tool, in my opinion, for transforming types, right, taking some type, making it another type in a very organized and deliberate way. Where does mapped types get its name from?
[00:00:26] A great way to remember it is it's kind of like array.map, if you're familiar with that, in that what we're about to see it feels sort of like looping behavior. Where we're iterating over all the keys of something and we're producing a type for a value. Again, we're purely talking about type information here, but the mental model of array.map and looping through and providing that transformative logic that you'd define with the callback for array.map.
[00:01:00] That's conceptually gonna fit pretty nicely with what we're about to see here. Let's take a look at the basics. So if you recall from TypeScript fundamentals V3, or equivalent experience in the wild, index signatures. You've seen index signatures before, hopefully, and this is what they look like. Note that we have kind of an arbitrary word here describing a property key.
[00:01:30] It's an arbitrary property key with an arbitrary name, meaning for a dictionary, if I access any key at all, I'll get this value type out. And then here is the value type. So this is like a generic type for a dictionary. I mentioned this in the fundamentals class.
[00:01:46] I'll often say I want t or undefined here, because there's always a possibility that when you access something on a dictionary, it might not be there. And I want to keep myself honest and to make sure I check for the presence of a thing when I grab it out of a dictionary.
[00:02:03] So we could see here, we can create a what I'm calling a fruit catalog. It's a dictionary of fruits, it'll start out empty. Here I'm kinda illustrating the problem I just talked about. But when we access any key we wish despite the fact that this is empty, we see we get a fruit back.
[00:02:21] So this what we're passing in for the t and we're kinda threading it through. This is an index signature, we've already covered this before. I'm gonna introduce a new concept here. And why did need a new concept? Because the only thing an index signature allows us to do is say for an arbitrary key, here is the type you can expect to receive.
[00:02:47] What if we have something more specific than just a narrow string? Or sorry, something more specific than any string rather. What if we have a list of properties and we wish to use that and not just the Wild West like you could put fruitCatalog.turnip or whatever you want, right?
[00:03:11] We don't want that, we want something a little more deliberate. Well, this mapped type is gonna allow us to do something like this. Now, because in my mind at least, when I say the word dictionary, that means this very flexible key value store where I can hang whatever I want on an arbitrary key.
[00:03:30] So I'm gonna actually call this something different, given that we're no longer gonna be in the world of arbitrary keys. We have a set description of the keys we wish to use and then we have some value types. So we're gonna call that a record, and to avoid collision with something that exists in TypeScript already, I'm gonna call this MyRecord, right?
[00:03:53] Just to avoid colliding with an existing type. So here's what's going to change. This is our interface as before, and here is the new signature. And we can look at these side by side in a moment. But for now, I want you to look at the fact that when I access cherry and apple, I get a type of fruit that's coming out.
[00:04:19] But when I try to access pineapple, we get nothing. Pineapple does not exist on MyRecord, and cherry and apple are here. So it seems that we're able to say, here are the specific property keys that I wish to use. Whereas with the index type, it was kinda like put it anywhere in this object and I will hold it for you.
[00:04:43] So let's compare these two signatures side by side. On the top, we have this mapped type and on the bottom, we have the index signature. And I've hidden out the type for the value here, because really, that's not what these signatures are all about. We can put whatever we want there in that dot, dot, dot.
[00:05:03] We'll talk about that later. Right now let's just look at the stuff that's on the left side of the colon. So index signature, the name doesn't really matter. I mean, you can put something there if it will help the reader understand what you're trying to do, maybe it's customer ID or something like that.
[00:05:23] Something that has some meaning to the reader, but it's not as if you're referring to this value anywhere. You're not saying name doesn't matter dot whatever, you can't use that. We're gonna see later that in this mapped type, we're able to use this. So giving it a name and making that name descriptive is a little bit more important.
[00:05:44] So here we have a colon in a regular index signature, and here we have this word in. And you can think of, when I talk about looping behavior, think of this union type as what we're looping over. And think of this as your i, right? For i equals 0 and loop over this.
[00:06:08] Or if you're doing a for in, right, where you're like, const key in dictionary. Fruit key is first gonna be apple and then it's gonna be cherry, and we're kind of processing, at least mentally we can think of it as a loop. Now remember, this is just type information.
[00:06:29] So there's no code that's being run and looping. There's nowhere you could put a break point to see what's going on. But the mental model of looping will hold up here. So mapped type lets you be more constrained and specific. Index signature, by its very nature, is about defining arbitrary key value relationships, different tools for different jobs.
[00:06:54] One's not necessarily better than the other. But if you were to say FruitKey in string, you would now be using a mapped type that is equivalent to the index signature below. Cuz it would just iterate, it would say like for every string, every possible string, here is the way I defined my value type.
[00:07:36] It's sort of a projection but it feels like looping to us. We can think of it as looping. So the in keyword, it's a telltale sign you're dealing with a mapped type. And index signatures, you can only define them on arbitrary strings or arbitrary numbers. That's it. And look what happens if we try to be more constrained in a regular index signature.
[00:08:01] We're literally told that these error messages are getting better with every release, consider using a mapped object type instead. If we attempt to do the thing that mapped types are designed to do, or we're doing it the wrong way, we're told, just go make a mapped type, please.
[00:08:21] Cool, so that's the difference. Seems like an index signature builds on top of that concept, but it's different. So, let's make our MyRecord type just to show what that is, right? This is not generic yet. It very specifically operates on apple and cherry, and it very specifically operates on fruit.
[00:08:44] Let's make that a bit more generalized. So what we're gonna do is replace this fruit key thing, right? Where it says FruitKey in apple or cherry. So let's rename that to key, down here. And then we're gonna pass in this apple or cherry thing. We'll make that a type parameter here.
[00:09:04] We'll call it the KeyType. And that'll be the first thing. And then this thing to the right of the colon, you've got this fruit. So we wanna make that generic. We'll pull that into a type parameter as well. So that's all we're doing. And by the way, this is from an experienced practitioner of TypeScript.
[00:09:25] This is a good pattern to follow, implement the non-abstract thing, and then start pulling things out to make it parameterized. It's a good way to make sure that you at least can affirm that continues to work with the use case you care about. So now we'll end up with something like this.
[00:09:47] It's much more generalized where we can specify here that we can pass in something like apple or cherry, right? Pass that in as strings. Kind of like those will be the names of the properties. This will be the values, the value of the property. And then it's almost like we're looping over everything in the key type we passed in.
[00:10:13] First apple, then cherry, and then we're giving this type. We're giving it a value type on the other side. Now, this is already built into TypeScript, but now you understand how it works hopefully. So it's called Record. And I want you to look at these side by side and see that aside from this, the names of these properties, which I've tried to be just a bit more descriptive than single letters.
[00:11:08] So that's a difference in the constraint, but doesn't make things fundamentally different. And then we have key in, some type that we passed in. So this is property key in K, well, there it is. And then there's the second type of param, and there it is. And here's our second type of param, and there it is.
[00:11:29] So really, what we're able to do with this mapped type is build up an object with a set of known keys that have a consistent value type. That's the first use case for this. Now, why might you care about this? Well, sometimes you want to maybe have a bunch of form fields and they each have sort of a value and whether they're validated or not.
[00:12:05] It's great to have a nice dictionary like this, especially if you're pre-creating one and you know exactly what you expect to be there. I'm blurring the line here between a record and a dictionary. But this is a quick and easy way to go ahead and create one. I would say this is a subtype of a dictionary.
[00:12:28] A dictionary is flexible in that you can have arbitrary keys and this has specific keys. So this is just a more specific thing than a dictionary.