Making TypeScript Stick Typed Data Store Solution
Transcript from the "Typed Data Store Solution" Lesson
>> Welcome back. So we're going to go through this challenge and we're gonna build a data store that has a strong opinion about how a method should be named. And we will see how we can sort of establish that convention with types. And get great feedback in our editor as this evolves, as we do things like what we see on the screen right now, adding new types of media that this data store aims to manage.
[00:00:27] So, first, I gotta go to my editor here. All right, so first thing we need to do is we have a contract that we wish to enforce here. And the way that's typically done with a class like this is through the use of an interface. I can actually get rid of that.
[00:00:51] So let's say, implements DataStoreMethods, right? So this interface is gonna do the job of having all of those methods on it. This is what's gonna give me this type checking that makes sure I have the right things here. So now, our challenges, we need this thing here to sort of be dynamic.
[00:01:14] One way we could do this, we could be like, so this interface is the way we're gonna establish this contract, right? This set of things that datastore must have. So one way we could do that is explicitly enumerating all of the different methods. But this doesn't seem to meet this requirement that we have around making this one simple change here.
[00:01:39] And then automatically seeing some feedbacks that says, hey, we should have clearComics, getAllComics, sort of that says getComics or something like that. We should have this methods for comics. So we can't do that, but what we can do is attempt to programmatically build them up. And I'm going to need a mapped type in order to make that work because once I do that, I sort of have my for loop.
[00:02:07] And one way we can do this is, we can say I have a type, and I can say we have a key, and that key is the property names of data entity map. Does anyone have a suggestion as how I could do that? How do I get the properties on an interface in a string literal type?
>> Object.keys, there's a type equivalent to that, keyof DataEntityMap. So if I just did this, let's just take a look at what it gives us. So, ds = new DataStore, just giving us something to look at here. ds dot, we're not gonna have it until we implement these.
[00:03:05] But you could say, hey, we're missing the following properties from DataStore methods, movie and song. We should have properties called movie and song and we don't have them. So this is the kind of feedback we're looking for, but it's not exactly these words that were interested in. Then this is where a template literal type could be useful.
[00:03:26] So what we can do here is use this key remapping feature. So it's not these specific words here that we want, but it's something that's derived from those words. So we could say as and then backtick getAll, and then here's our dynamic piece K. And let's see what we get now, hey, look at that ,getAllmovie, getAllsong.
[00:03:57] The case is wrong, we needed sort of an S at the end to make it plural, but we can see that that the last slide of this error message suggests that our key remapping is happening the way we would want it to happen. So let's add that s to make it plural and let's capitalize this with those nice little helper utilities that allow us to transform K.
[00:04:25] So look at that, we've got getAllmovies, getAllsongs, great. Now these are not properties, these should be methods, right? So this is a case where the value, the type of the value in our map type should be some sort of function. And in this case, when we're getting the entire list of things, I think it's probably a zero argument function that returns something, it returns an array of some things.
[00:04:58] Now what that array is, it should probably depend on what you're asking for, right? This is if you say getAllmovies, you should get an array of movies. If you say getAllsongs, you should get an array of songs. And so how might we do that? I'll take an answer also if you wanna think of this in terms of values instead of types.
[00:05:26] Do we have enough information here is I guess the first question remembering that this is like a loop. Or this is first, it's movie and then it's song.
>> Indexed access types.
>> Index access types, and that's sort of like square bracket notation for going through an interface, given a key, get the value type stored under that key.
[00:05:52] And so in this case, we've got K, and so that's not quite what we want. I mean, if we look, well, there's not an easy place to see it right now. Actually, we could do it this way, ds DataStoreMethods. Just to fake it. So we have something a nice autocomplete thing to look at.
[00:06:14] So, okay, what we get here is an array of literally the string movie. That's not what we want. We want movies. So what we're gonna do is say DataEntityMap. And then pass in the key and then we can get that back. So we're gonna pass in movie, we're gonna get the interface movie.
[00:06:38] And then we want an array of those things. And where does that leave us? getAllMovies, getAllSongs. So this brings a lot of different things together as one. We have a mapped type. We have key remapping which makes use of this template literal type. And then we have an index access type which lets us use this interface here as the source of truth.
[00:07:12] And it makes it so that we're not writing every single getAllMovies, getAllSongs, getAllComics. We're establishing a convention that is based on this thing up here. And to prove that this works let's have this comic thing. So I believe I had issue number which is a number. And we'll add comic down here.
[00:07:47] And we should see, hey, getAllComics. We're being told, look, there's this convention, I see in your DataEntityMap. You've got this concept of a comic and your interface here Or this type. It tells me that I should have a method here that when called returns an array of comics.
[00:08:10] So here we go. There it is, comics, movies, songs. So this is really at the core of what this exercise is all about. Now, how do we make this work for the other methods? Just smash them together. So here we would say, get song and this will return one thing instead of an array of songs it will return, just a song and we'll take in an id as well.
[00:08:42] Where does that leave us? Get comic takes in an id, returns a comic, get all comics. See how that's all nice there. Now we're going to have to implement this. But this is sort of the most interesting part of this challenge. Let me add a new line here.
[00:09:03] Just so we're not having to read, scrolling things from left to right. So in both cases its sort of a loop over these keys, a remapping which is where we establish a method name, and then this is just simply the type of the method. And the way we can in the abstract refer to the kind of thing we're returning be it an array of them or a single one, it's using this index x access type, passing k into that data entity map.
[00:09:38] So I'm going to add two more. I think one more, clear. Clear and then plural again. This is going to be, I believe a void function. The test suite will tell us. And just, to take the pressure off absorbing all of this. This is three uses of largely the same pattern.
[00:10:04] Really the only thing that's changing is the type of the value that the method returns and this template literal type, that's it. We're not even changing the way this is capitalized here, so it's sort of three loops. And you may know of interfaces as being this thing when you're using a class and you're saying this implements a type.
[00:10:30] You may think, this type, it's like, should be an interface. Well, it doesn't have to be this has to just be something that is possible for a class to implement. So, if we said it could be no, now we'll be like, we'll be told hey, if you can't do that, there's no way for class to end up being now.
[00:10:57] So as long as it plays by the rules that a class must play by, you can use a type alias like this. And we need to, in fact, in this case, because you can only have one index signature like this in any given interface or any given type like this.
[00:11:18] And that's why we have to sort of end them together to create one cohesive thing. So how might we make this work in reality? I think we have all the methods we need now, we've got the clears, we've got the movies and songs, we've get comic I think that's all I'm seeing just based on the limitations of this list.
[00:11:39] I'm also going to remove this so that we pass the test suite. But that was a good sanity check for us. I'm just going to comment that out for now. And let's go ahead and implement this. So the first thing we need is a place to store data.
[00:11:56] And anytime that I see some pattern like this, right where we're retrieving something by id, I kind of want to store it in a way where that access this is going to be really fast. So, I can say we're going to have, an object called data and I'm going to make it a real private class field so that no one from the outside can get at this except through the methods I provide.
[00:12:22] And this is going to be of a particular type. So we could say, key, In key of data entity map, and then the value is going to be a dictionary that holds data entity map. So, we could do that another way. Record, can anyone tell me what a record type does?
[00:12:48] Like what this little helper does? So if I were to do like string, data entity map of type k. Let me make this more simple. So, what record does, it effectively it's a shortcut to a dictionary. Now that we get that nice unchecked indexed access thing, where there's the possibility of something being undefined.
[00:13:34] I don't need to define my own dict type anymore. I can just use record. So it's just saying they'll give me an object with key of string and value types of string array. So effectively what we're doing down here is we're saying loop through all of the keys in data entity map that's these here movie and song and then so let's create an object with keys, movie and song.
[00:14:05] And each of those should have a dictionary, right an object where you can have any key you want. And the value type is going to be the interface movie, the interface song. And we'll start at empty, We're being yelled at because, We can't start out totally empty. You got to start out this kind of empty, And a comma would help.
[00:14:45] So if I wanted to make this more complicated which I don't, you could call this movies, songs you just do more Q rap remapping here but no need to overdo it right, especially while we're just trying to wrap our heads around this thing. So now, time for me to implement all of these methods.
[00:15:05] And unfortunately because it's a little bit abstract here, vs code is not going to help us and just say, implement all of these things for me. It would do that, if it were a simpler interface, but it may be having trouble figuring out exactly what we're supposed to be doing here.
[00:15:22] Thankfully, it's a pretty repetitive pattern. So, we'll just do this manually. Get all songs return to song. Sorry an array of songs. Get song, we'll just do them like this one triplet. And then clearSongs, this returns nothing. Great so that's for song and then I'm going to implement these and then I'm just gonna copy and then replace it with movie.
[00:16:09] So here, we're gonna need something like to return the list of songs. So we'll get all the keys for the song, Object.keys(this.#data.song) and map over those and get the corresponding values. And we're being yelled at because there's the possibility of something being undefined here. So why is that?
[00:17:00] It's because of our unchecked indexed access. So this here, effectively what we're saying is, this whole thing, if I were to remove this explicit type annotation, getAllSongs returned to this. It's not the mapping over the keys that's the problem, it's the fact that I'm using this index access here without making sure that everything's present.
[00:17:29] So I'm gonna use my all time favorite type card here, isDefined, which is generic over type T. I'm gonna explain what this does in a moment. All right, so in terms of the code that actually runs at runtime, very simple, is the type of x undefined, or is it not, right?
[00:18:06] Anything other than undefined will cause this to return true. It'll only return false if it is undefined. But that's the least interesting part of what's going on here. This here is the interesting part. So we're saying this is a type guard, right? We know because of the return type, it's not simply saying I return a Boolean.
[00:18:30] It's saying I return a Boolean with a specific meaning, a Boolean that should tell the type system something special. In this case, it means if I return true, that means x is of type T. If I return false, that means x is not of type T. That's what a user defined type guard does for us.
[00:18:58] And the fact that I've got T or undefined here, that means that when we have something here, like in this case, song or undefined, right? And we pass that into this function, T is gonna be matched with song, right? That undefined piece sort of breaks out. It's not part of the T.
[00:19:19] It's part of what the argument accepts, but it's not part of T. And that let's me, say, strip away this undefined piece. Now, I could just do filter, isDefined, and we should be good now. We return an array of songs. So in summary, get all the keys in this little map of songs, and then iterate over those keys and retrieve the respective song.
[00:19:54] And then filter out anything that might have been missing. Another way of doing this, which I would consider to be perfectly valid here, this is a rare case where I would say you could do that, which is the non-null assertion operator. Usually, I would advise against this, but we are iterating over all the keys in this data structure.
[00:20:17] It's short of deliberately storing a null, or an undefined in your map under a key. I wouldn't expect this to cause any trouble for you. Normally, you don't wanna do this, because TypeScript's saying hey, there might not be anything in there under this particular key. But where's the key coming from, this.
[00:20:38] I can rely on that working pretty well. It's not arbitrary strings here, it's giving me the strings that supposedly have something stored under them. So either way would work. In fact, I can strip this out, and we can see same return type. Does that makes sense to everyone why this is generally not a good idea, but here specifically, it seems okay?
[00:21:07] Yep, okay, so now getSong, this should be easy. But we will need an undefined check here, because the ID now, that's a user generated value. They could pass me whatever, right? I wouldn't wanna just sort of press right through that. But similar, right? And this will take an ID, Or a songKey, whatever we wanna call it.
[00:21:43] And let's say we just throw if someone gave me a bad key. Something like that, and great. So go grab it, attempt to find it. If it's garbage in, garbage out, we throw. clearSongs, it's pretty easy. That's it. All right, so we took care of songs, let's do movies.
[00:22:30] I'm gonna be kinda lazy here. Let's copy this, just do a little copy paste, and, Gonna find all occurrences of song and call. Just make that movie, and then I'll #data.Movie. Let's fix that little case. And fix these, MovieKey. Just make that lowercase m. And wow, that worked.
[00:23:10] Just some renaming, it's why I only gave you two, just cuz it starts to get a bit repetitive. And let me run the test suite real quick. Still failing a test, why? AddSong is not a function. Looks like I need an add. Okay, no problem, we can add that in real quick.
[00:23:41] It's just gonna be one more pattern of this map type here. So we'll say add, and then the entity name. And this we'll take in as an argument, a DataEntityMap key. And I believe that we just return exactly what we just added. So let me get rid of the file tree here, so we can see real clearly what We have just added, so there we go.
[00:24:12] One more map type iterating over the same keys, add and then capital S for song or capital M for movie. And then this is just the type of the method. So we take in a new thing to be added, we return what was just added. And if we go down to the data store here, we can see, hey, we're missing.
[00:24:35] Add Song and add movie. So already we can see, hey, we updated our pattern, we added something new. We're being told right at the location where that change needs to happen. Go ahead. So let's say add movie. And we'll just say, We'll just say this data movie, and that ID = m and then return m.
[00:25:16] And we can do add Song very similarly. We'll change this to S for so There you go. So we're just pushing this thing into the array. Let's run our test real quick. All right, still failing. What's going on here? At song is not a function. Turn up saving the file and help, There we go, all tests pass.
[00:25:54] So in summary, what we have here is a convention that we can establish across multiple data types and we programmatically build up. Think of it like an interface literally, it's a type alias but we're implementing it with a class. And let's give this last requirement a test what happens when we add comic back to this data entity map well this lights up and it says look you need like you need get all comics.
[00:26:33] And then if you add this method, it would say, you need clear all comics. And even here, it's saying, hey, shouldn't you create a little dictionary of comics, where are we going to store this? So you could define this in a very hard coded way. But what you can see here, this is the way a lot of libraries that manage the data layer like handle things.
[00:26:57] They want you to sort of describe what your entities look like, and then have something that is more abstract, building up something that will they'll directly interact with. To make things even a little more interesting remember that if we did something like this, Open interfaces give us another opportunity here, so I could have defined it this way in the first place.
[00:27:24] But let's imagine. We had like. I have to export both. For export one, I have to export both. So interfaces are open meaning you could have multiple files that are sort of just adding some stuff to this interface and it gets all squashed together. So let's say that you have your movie interface in one file like this stuff in one file and then maybe these to song and comic in another file.
[00:28:05] You can kinda have all of this co-located in different parts of your project and it's all gonna sort of pile into the single interface which then the rest of the stuff refers to. So you'd get the right separation of concerns in terms of how your project is organized.
[00:28:21] And then TypeScript is causing errors to light up right where you need to make changes as you refactor, right? As you rename things, as you add a new type of data entity. So there we go. That's our data layer. And before checking this in, just comment this out, so that we're passing in.
[00:28:44] But hopefully this helps you understand template literal types, map types, index access types and how you can use them, too. Define a more abstract convention than simply saying method name should be this, method name should be this. You can do something more powerful than that.