TypeScript 5+ Fundamentals, v4

Type Registry Pattern

TypeScript 5+ Fundamentals, v4

Check out a free preview of the full TypeScript 5+ Fundamentals, v4 course

The "Type Registry Pattern" Lesson is part of the full, TypeScript 5+ Fundamentals, v4 course featured in this preview video. Here's what you'd learn in this lesson:

Mike introduces the concept of the type registry pattern and explains how to use module declarations and open interfaces to create a central interface that represents a registry for different types of records. A demonstration of how this pattern can be used to easily add new types of records and achieve type checking in a web application's data layer is also covered in this segment.


Transcript from the "Type Registry Pattern" Lesson

>> All right, let's put some of what we've learned here together and explore a use case. And I call this the type registry pattern. And we're gonna look in a special folder I've created here just for this purpose, cuz I wanna have multiple modules here. So let's look at what we've got.

So in this type registry folder, I have a data folder and a lib folder and then this index.ts. So what we want to do, well, I have to introduce a concept here first. Remember when we were augmenting the window object and we said declare global, right? Let's see, it was here at the very bottom.

Remember when we did this? This is like saying, I'm going to provide some type information and I want to state which scope that type information should pertain to. Well, we're gonna do the same thing here, but we can do it for a particular module. We can say, I want to declare some type information, and it should be treated as if that type information were like literally in a different file.

In this case, the lib/registry file. If I put an interface between these braces, it will be as if I took that interface and I went and I pasted it into the /lib/registry.ts file. That's a new concept that I wanna introduce here. So what we're gonna do is look at a use case where the key of type query is going to be useful as well as an index access type, I believe, in order to solve the problem.

The problem we're going to try to solve here is building a data layer for a web application. And let's say it's my team's job to make sure that a bunch of other teams have an easy time fetching data. And I wanna build a nice easy to use function that takes the name of a resource to fetch and its ID and returns the right type.

We don't know exactly what types users will need because maybe the teams that I'm helping empower, they're defining what book is and what magazine is. So I kind of need to set something up that allows very easily for new things, new types of records to be created. And they should kind of just work with fetch record, that's our goal.

So here's our file structure. We've got a data folder with book and magazine in there. You'll see those are like trivial little classes where we define what a book is and what a magazine is. You can imagine that we get JSON back from a server and we sort of deserialize that JSON and end up providing an instance of the class.

That's what fetchRecord will return, a promise that resolves to an instance of that class, maybe. We've got a file called registry which is where some interesting stuff is gonna happen. And then index.ts, let's say that's the entry point of our program, that's where fetchRecord is gonna be used.

So let's go and look at a couple of these different files. We'll start with registry. So what we've got here is an empty interface. This is empty by design, and we've got our fetchRecord function. Our fetchRecord function takes an argument which is key of data type, registry and string.

And that's just to make it easy for us to see what's going on here. And I'm actually gonna comment a couple things out here, so we can see a before and after more easily. So our starting point, arg is never, you haven't talked about this yet, but it represents an impossibility.

The set of values that corresponds to the never type is an empty set, represents that nothing will make this happy. Now, if we put something in here, Well, there we go, arg can be foo. It's the key of this data type registry. And I'm just adding this so that we can see it easily.

We'll see when things pop up. And then the second argument here is the ID of the thing to fetch. So what we're gonna do, let's look at our starting point here where we're saying I wanna fetch a book, fetchRecord. It returns a void right now, don't worry about that so much.

But certainly, these are a bit of a problem, right? These aren't accepted values yet, and let me delete this. So what we can do is each of these models, like this class representing a book, can have at the bottom of the file a little module declaration. And it's using this concept of open interfaces and it's saying, look, I'm gonna add something to the interface and treat it as if this stuff was in that /lib/registry file.

I want property named book and here's book. It's the interface representing instances of this class, key and value. Magazine, we're gonna do the same thing. Key is magazine, here's an instance of magazine. And now, if we look at index.ts, we don't get errors here. Look at that. We've got book or magazine.

We get some type checking there. So you could imagine, different people at the company, they're creating different resources like this. We could call this, we could say, video, this is duration. And this is video. All I have to do is that I just create a new class, and because of this, it's sort of like just picked up for free.

Look at that, book or magazine or video. So what I'm taking advantage of here is I have a central interface that represent sort of a registry that other modules are populating stuff into. They're adding their own entry there. And remember when we discussed, there's only one representation of a type in memory when things compile?

If we go to registry.ts, look at arg, book or magazine or vehicle or video. Now, this would work the same way if the code we're looking at right now were in your node modules folder. So you might ask, why would we want this? Why is this important? I could just go and create, ask teams to add their entry into this interface, right?

Well, if you're publishing a library, you don't necessarily know what the use cases of people consuming your library are gonna do. You don't want them to have to sort of modify the library in order to make things work. This would let you build something that's general purpose where you could sort of register things with a library.

And then, when the compile step happens, if you go into that libraries code and your node modules folder, you'd see something just like this. The types are sort of fleshing out. Even though if you look at this file by itself, there's nothing here that says book, right? It's been sort of injected because of these module declarations here.

So what's happening is, effectively, we're using an indexed, sorry, we're using a type query here and an open interface in order to create this. I could take this one step further. Actually, we will take this one step further a little bit later. We will return the correct type, but we'll need generics in order to do that.

LJ, I see you said the declare module thing is a bit confusing. If you want to learn more, Intermediate TypeScript is a great course to take. We will get much deeper into module declarations there and it's a more advanced concept but I promise we will scratch your itch there.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now