Transcript from the "Typing a Data Layer" Lesson
[00:00:00]
>> As a final kinda capstone to intermediate TypeScript, we're going to do a little bit of a practical exercise and that is to make a well typed data layer or a data store using the concepts that we've learned in the class today. So, and I'm just in the playground here, and you could copy this code if you like.
[00:00:26] But all I've got is three classes, one is called book, one is called movie, one's called song. And I've just chosen a different meaningful property, which we'll turn into a class field for each of these different classes. So if we did b is new book, we'd have to give it an author, maybe I'm the author.
[00:00:50] And we can see that b.authored it gives us this type back, and that's because we're using these nice little shorthand param properties, maybe you heard us talk about this in TypeScript fundamentals. So, ultimately, what I want is an API that looks kind of like this. I wanna be able to create a new store, this is like a data store, that's what I mean by store.
[00:01:17] And then I wanna be able to do something like store.get('book'), and this should return a book. And I wanna be able to do getAll, and this should return an array of books. And maybe create and this should take an arb if it's a book. Hopefully we get the idea here.
[00:01:43] We want a kind of a nice little crud, some portion of a crud like data store that gives me some good types when I retrieve things, and it gives me some type safety when I'm trying to work with things, trying to modify them. So let's actually, I created a book here, Let's pass that in, let's use this as kind of a little test case, right?
[00:02:15] And let's see if we can do this one, we'll say update, and this is gonna be kind of a patch, where I'm gonna pass some incomplete subset of fields here, maybe I'm just editing the title or something, call it a partial. So this would be a book ID.
[00:02:42] And then, Something like that. So we want create and update and then get a collection, and we want good type safety around this. And none of this works yet, so we're gonna have to make it work. So, clearly, we're gonna need these methods here, right? The getAll, create, and update.
[00:03:19] So now the methods exist but they don't take anything, they don't take the right types of things. And they all return void right now, so we need to do some work. Now I do notice that we've got book, book, book, book, we're referring to kind of a record type by name.
[00:03:36] So what I'm gonna do here is create a map of my records, and by map I just made a type that gives me keys and values. We'll call this EntityMap, since we've talked about the word record before in a different context, and I don't want to overload it.
[00:04:06] Great, so we've got movie, book, and song. The keys are the names I'm gonna use to refer to this kind of entity, and then these are the follow on classes and, sorry, they will be interfaces, because they're in the type position. I mean, we're defining them as classes but they will be the interface part of it, meaning the type of an instance of these classes.
[00:04:33] So in all of these methods here, I could say, kind, by the way, if you ever see kind in TypeScript code, it's because we don't wanna write the word type. That's a special thing, that's like a type alias. Kind is a good alternative to use. So, and this will be, A key of EntityMap, great.
[00:04:58] So now this book thing is gonna be happy. Now I actually want this to be generic, right? So we'll say k for kind. And the reason is, I need to create a relationship between things. So if you remember back to the TypeScript fundamentals, when we talked about generics and type params.
[00:05:28] The whole point of this is we're defining some parameter that describes a linkage between things. So, in the case of get, the relationship is I'm asking for a book, and let's say we provide an ID. And I'm gonna return a book, or maybe like a promise that resolves to a book.
[00:05:54] Let's pretend this is all synchronous for now, you can make it async by just making all these promises instead. But we're gonna return EntityMap[k], this is an indexed access type here, right? We're saying, project the word book through this EntityMap. All right, here's book, grab the value, bring it back.
[00:06:18] And so over here, look at our return type, when we use this string book here, we get a book back. Watch this, when we use movie, check that out, we're getting a movie back. So depending on that word that we're using, we get the right type of entity returned to us.
[00:06:42] Now, we can do the same thing with the collection, it should be very similar. But let's say we don't need an ID here, since we're getting them all, and I'm just gonna put square brackets after this. And let's see what's happening here, getAll('book'), we get an array of books.
[00:06:59] So this is a great use of generics, a great use of an index access type. Create, so here, we want to take an object to create, right? And this is gonna be an EntityMap[k], and this will be a void. So here, if we were to say, I'm creating a book, and in here we get author, right, get some validation.
[00:07:31] If we gonna change this word to movie or to song, song wants artist, From Mark G's new album, right? So, we get that nice linkage between the first argument, which is a literal type, it's a statement of which record we're operating on, and that gives us some really great control over the second argument.
[00:08:00] What about update? So we can actually just borrow this from the create, and we will say to update, or actually we need an ID here, cuz we're referring to an individual record. So we need an ID because we're referring to an individual record, and the only thing we're gonna change is we'll say partial.
[00:08:25] So if you remember what partial is, if we look at the type, it's gonna loop over all of the properties in whatever it's given and make the value type optional. So this means that if we had, let's say, books have a title, In addition to an author, so this would let us say I'm gonna update this book, and I could just do the author, or actually, I could do nothing at all, this is sort of a no update.
[00:09:00] But we get some validation, but we may just have a piece, like I'm just editing one field, right? Whereas up here, if we said book, We need an author, And a title. Something like that. Now we could, implementing these, I would leave as an exercise to you. That's not about the types, that's about the JavaScript, the actual code that you would run.
[00:09:38] So, what we've used here are, within partial, right, we've used a map to type, we've used this little modifier here, we've used an indexed access type. This mapping here is really nice and important, and everything's sort of flowing through that. So this interface ends up being really important because everything sort of threads through it.
[00:10:01] Now, what if we had each of these types of records in its own file? I mean, this is common, right? Where if we have models for data, we have a JavaScript module that is dedicated for book and for movie. Well, watch this. And then delete this, everything's gonna start failing at the bottom, cuz it's an empty interface, right?
[00:10:32] So key of that empty interface, there's like, nothing's gonna make it happy, and in fact, the key of an empty interface is never. So a no string will make it happy, but we'll get back there, no cause for alarm. So we would still need EntityMap to exist. I'm just gonna create a line here representing different modules, and you'll have to take my word for it that this will work across multiple files.
[00:11:00] So let's imagine that these are all different files, we could do this. We're gonna take advantage of the fact that interfaces are open. I see some head nods, people know where I'm going with this. So, if you maybe have some kind of library or framework that you're using that has like code generation, where it builds starting point files for you, all you need is to have this.
[00:11:34] This is part of your code gen, this little thing. So each record type we have here, a movie, a song, a book, they're all sorta adding themselves to this EntityMap. It's almost like they're registering themselves with this central thing. And it turns out everything ends up being added here and our code starts working again.
[00:11:59] I need a title here, A History of TypeScript, I can't help myself. So, basically, we get the same autocomplete that we had before. If it works. There we go, book, movie, and song, right? So, this has the same effect as if we defined that one interface upfront. So you can have your code split up across many, many different modules.
[00:12:31] You get to group each concern according to movie, and song, and book, right? According to what you're modeling, instead of saying, all right, here's where we maintain all the types, and here's where we maintain the actual code that runs. That to me is the wrong splitting point, I like being able to go to this one file and it's all here, everything having to do with this is here.
[00:13:01] And this shows us that this EntityMap thing and interfaces in general are kind of this cross cutting thing that, You can use to sort of make horizontal things across your app. Meaning, we can refer to this now and it relates to all of the different types of records we could work with, not just one thing in one module.
[00:13:28] Now typically, these will be in separate files, we can kinda simulate that. And we're gonna do this with what's called a module declaration. This is as if, so make TypeScript think that this lives in a library called fem-data. All right, this is actually gonna still work, because we've got these EntityMap things here.
[00:13:59] Sorry, we've got the individual interface declarations for each record, but let's copy this over. Let's see how this works. Cool, now EntityMap is not gonna be accessible here, maybe we can do this. [SOUND] Import, this may be beyond what the, May be beyond what the playground can handle.
[00:14:43] Actually, we may be getting the right auto complete behavior here, let's check one more time. Nope, it's all disappeared. So, beyond the bounds of what can be done in the playground, but this is exactly what you would want if you were breaking things up across many JavaScript modules.
[00:15:06] So you could say, you would define this here where this interface actually exists, some ts file. And then here you could say location/of/entity-map, right? Just like it's a local file of some sort. And that'll make sure that you're modifying the EntityMap that's in that other file. You're augmenting that interface rather than an EntityMap that's in this one module, if that makes sense.
[00:15:39] It's a really powerful pattern that scales nicely, because you can imagine as you add and remove different modules and things, this list of things you could retrieve from the database grows. You don't want one central collision point where they're just tons of git conflicts there all the time.
[00:16:00] This makes it so everything sort of has its own place with one central location of registry.