 Hello, OpenJS world. My name is Ethan Arawad, and welcome to my talk on applying advanced TypeScript definition patterns. I'm a software engineer at Microsoft, and my pronouns are he, him. If you'd like to follow me on Twitter or GitHub, you can find me under those handles. So today's talk is going to be about Fastify and its new type definitions. Fastify is a Node.js web server framework. We are a part of the OpenJS Foundation, and we are open sourced. Today, we're going to be covering five patterns, but we're going to be doing so in three parts. So the first part is going to be covering discriminant unions and function overloading. The second part is going to cover name generic parameters. And the third part is going to cover declaration merging and generic inference. We're going to be doing this through a sort of a code walkthrough. We're going to be building these type patterns from scratch over a simplified implementation of Fastify. You can check out the source code on GitHub. I'll have a master branch, which will have all of the completed code, as well as an initial branch that'll have the starting point if you'd like to follow along as well. All right, and welcome to the coding portion of my presentation. As I mentioned before, there's going to be three parts. The first one is going to be handling discriminant unions and function overloading. The goal will be to implement the types that'll help define the right options objects to determine which node server should be returned when calling the Fastify function. Part two is going to handle a name generic parameter. And this is going to be all about how we decorate the request object with, you know, four different properties and only have to specify the ones that the user expects to be passed to their request object. And finally, for part three, we're going to be covering declaration merging. In this case, we have two methods of modifying the local instance where this server, this Fastify server, you can decorate with, you know, unique values. You can also provide plugins that have access to that instance and can also do things like decorate the instance with custom properties. So we're going to use declaration merging to implement a plugin system of sorts. We'll take a quick peek at the types. So this is going to be our goal today is going to be converting all the types to see what you see here. But we're going to do it. We're going to take it line by line and explain as we go. And we're going to start here with the initial Fastify type file. So you can see that the function down here takes in an options object that has three optional properties and it returns a Fastify instance. This Fastify instance has a single property server that returns, you know, one of the four Node.js server types. We can take a quick peek at our implementation. You can see that the goal here, you know, using that final type doc, we can see that we want the server to be unique based on how it's instantiated. See how this is an HTTP2 server and this one is secure and this one is regular HTTP. That is because when you, you know, call that Fastify function, we're passing in different sets of the options. And we want the type system to be smart enough to determine which version of Fastify to return here. So going back over to the types, we see that we don't currently get that. Let's quickly switch our part one over to the different types and see what happens instead. So we'll call this init and we'll see that now the Fastify instance is always the same. It's always Fastify options and that server is always going to be server union, not ideal. So let's fix this. Start by renaming that back. We'll go over to this file and we'll rename it as well just to Fastify types. Now we're going to start by implementing the discriminant union over the Fastify options. We're going to need four types, Fastify HTTP options, the Fastify HTTPS options, and then we're going to need the to and the to secure. Now for the HTTP options, we're going to go ahead and we're going to copy over that secret property. And we're going to keep it as optional because any one of these four can have this property. And then what we're going to do that is by using a intersection. We're going to intersect the other three types with that base type. So this is a sort of inheritance model where all three of these other types are all going to include everything in this type and then whatever is specified on their own. So let's start with HTTPS. We're actually going to take this property here and we're going to use those base server options. And we're going to make it required now. This means that this type will only ever be possible if the user defines this property and only this property. That's the power of discriminant unions. So let's take a look at how this feeds into the HTTP2. Instead of it being Boolean, we're actually going to go ahead and use a type literal of true. So now the user will get this options object if they've specified only HTTP2 option and only specified it as true. And then for the secure version of the HTTP2 server, we're going to use that true again. And we're also going to use the HTTPS options. So it's sort of like an and operation where if it's both of these properties are defined, we know we have a secure server. And we're going to go ahead and change this type to that HTTP2 and we're going to make it the secure server options. So let's go ahead and remove this interface. And we're going to have to update that down here. So now in this function, we're going to copy it four times. The one at the bottom here is going to use the HTTP options. Then for the three previous to it, we're going to specify it for the three other ones we have to find here. And we're going to make them non optional. So we're going to start with HTTPS then HTTP2 and then HTTP2 secure. And now this facify object, this function is currently already implementing a discriminant union. You can see that it has those overloads on the right side of this code sense. We're going to there's multiple options. Look at this can be TypeScript is intelligent enough to say, hey, we have these four possible options. Four possibilities for what this options object can be. So let's look at the difference between them all. And if they have enough of a different, if they have something that is, you know, discriminately different in this case, you know, the existence of certain properties over others, then TypeScript is smart enough to, you know, return exactly what we want. So now let's use that to our advantage to make sure that we specify the specific types, you know, the specific node server based on the different kind of options. So we're going to add a generic here called server. It's going to extends from that server union that we've already written. And then instead of that server being defined as, you know, any one of these four, it's going to be whichever this generic is. What we're doing is we're also going to default it to the HTTP server type. This will allow this, you know, this non optional one to work best. So now down here, we're going to pass that server. But instead of specifying the union, we're going to pass in the, the Node.js server type that we want to correspond to the given option set. And TypeScript will respect this generic parameter as a part of the overload and will return the facify instance with the node server that we want specifically. Finally, HTTP server. Great. So now if we jump back over to part one, we'll see that we're still using our new types. And if we hover over these, you'll see that not only are the options now, you know, specified based on what has been passed in, but the server types are as well. And now we've achieved part one, discriminant unions and function overloading. Now on to part two. So in part two, we're going to talk about named generic parameters. So see here, we have our server. And in the previous section, we were able to make, you know, the server based to be a specific kind of facify instance based on the options passed to it. But now we want to make something unique out of this easy, this get property where it's going to be a route handler common to many HTTP server to server frameworks where you pass in a path, and then you pass in some set of handlers. In our case, we're going to have two handlers, we're going to have an options object that allows you to specify a pre handler. And then we're going to have that third argument on our, you know, overall get function that lets you specify the route handler itself. And the idea here is that if this pre handler doesn't return true, then this one won't run. But what's unique here is we want to consider that this request object, we want it to inherit the, the same headers and query that we specify in this object in this generic here. But furthermore, this generic needs to be sort of dynamic where if a route that is someone is creating only has headers, we want them to be able to only specify the headers type and not have to deal with the other types. In fact, this one is going to be so powerful that it's going to contain the types for things like body as well, which, you know, doesn't exist on a get request. But if you were to go ahead and create a server dot post request, you would want to have the body type on there as well. So we'll go ahead and pretend that we're doing that as well. So you can see here, we will call our function, put that object in pre handler request response, put the request that body here, and we're going to go ahead and make sure we pass in that final function request response. And we'll check out that request that body, you can see here that body now contains that string. So great. How do we do this? Let's jump back into our definition definitions. Before we do that, let's change this comment over so you can see things are failing. But now over here, let's take a look at the facify instance. The facify instance is what contains that get and post function. So let's go ahead and write that when you get, we're going to call this facify route handler. Then we'll do post, say, facify my my mistake, facify route. So let's go above. Let's go above these types. We're going to go go ahead and write type, facify route. This is going to be a function. That's going to take three properties, the route, which is the string, the options, which we're going to call facify route options, and then the handler, we're going to call facify route handler. And we know this is going to return void. So keep in mind, so now let's let's take one step back. We realize now that we're specifying things like the get and the post at this level. And we talked about that generic before. So how are we going to pass that generic through to these functions? Well, instead of putting it on the type itself, we're actually going to put it on the function that that it corresponds to. We're going to call that R. This is going to extend from something called the request generics generic. And it's also going to default to the request generic. With that, we're going to go ahead and define this type request, generic equals, and we're going to go ahead and specify the four properties that we have headers, it's going to be all four are going to be unknown query, params, and the body. And then we're going to take that are generic, we're going to pass it through both of these pieces. So next, we're going to start with the facify route options, because that's an easy one. I'm going to say facify route options. We're going to have that generic are, and we're going to copy over the same constraint as the previous one. And you'll see here that the type now exists on the type itself and not on the inner piece. That's because the user won't necessarily have to pass us in themselves every time they use it. Here, we're going to have that pre handler method. And we're going to have that refer to a facify route handler, because it's about the same. We're going to give it R. And then finally, we're going to type the facify route handler, which again is a function. And we will consider that that function is going to have two types, request, which is going to be a facify request, and res. We're going to keep res as unknown for simplicity sake, because it's about the same as the request type, but we're only focusing on request right now. And we also need to use that generic here. Notice how we passed that. So let's take that again. Let's take that piece, put it here on the outside and pass it through to the request. And for the final trick, we're going to find the facify request type. So you can see how we really break this up. So it makes it really accessible. We're going to pass in R here again. So we'll copy that over. And now we notice that facify request is an object. And now this object is going to have the corresponding four properties, headers, queries and parameters. So taking a quick look back at part two, you'll see that we just call them the same thing, but lowercase. But we want them to correspond to whatever is in the generic. So here, remember that the generic is referred to as R. We're going to go ahead and call the headers property, and it's going to be R headers. We're going to need query property. It's going to be R with the query string. Params is R and params. And then finally body is R and body. So with all of that implemented, we can go back to part two and see how we've done. We've made one one oversight. The properties in the generic all need to be optional. That enables that default operation to go into effect with the unknowns. So what happens, so the difference is here, we've made these all optional now, and they'll default to unknown through this sort of, you know, generic constraining and defaulting. So when R is specified as only, you know, an object containing that headers prop, then it'll only set that one here. It'll set the others, but it'll default the others to whatever that default is here, which again is just unknown. It's a little bit of a like very much shared interface experience, but it turns out to work really well and work out in our favor because as you can see, the headers correspond to the type defined here. And thus concluding part two, implementing named generic parameters. Alright, so here we are in part three, and we're are going to be covering declaration merging and implementing a plugin system for our facify instance. So taking a look at the, you know, the facify JavaScript implementation, you can see we have two functions. There's decorate, which given a key value pair will add that key value pair to the facify instance. And then we have the register function, which given a plugin in an options object, we will apply the, you know, the, the local facify instance to that plugin with its options. This, this methodology of a plugin system will allow the plugins to call things like this dot decorate. But from a TypeScript perspective, you know, plug plugin or not, calling this dot decorate is going to be, you know, a little difficult. While we could add a type to our server to say, you know, any additional property could be of type any, it doesn't really solve much for us. But instead, if we had a, you know, a file like we do here, we're calling, you know, our dot decorate on our server. In that same file, we can actually write a declaration merging for our facify instance with that, with that value. And you can expect it to be here as string. But as a part of this section, I also want to show a little bit of the magic that we can do with plugins and a little bit of generic inference. So let's take a look at our whisper plugin. First, we'll say here in our implementation, you can see that we're registering the whisper plugin or passing it the local secret that we've set on the line above. So keep in mind that facify will take this plugin function and we'll pass it the instance facify server, as well as this object here with the specified options. So taking a look at our plugin, we can see that all it does is call dot decorate on that instance and then it, you know, whispers the secret. So we can take a look at the final types of definition again, before we write it ourselves and you can see that we have that facify plugin interface, as well as those two methods. So let's write it ourselves and explain it as we go. In facify type definition file, we're going to start with that decorate method. It's pretty easy to implement. Pass it a key. It's a string and a value. In our case, we're just going to use any. You could probably use unknown here and make the user specify exactly what that value is during runtime to make it a little bit better. But for all intensive purposes, any should be okay here. Then we're going to have that register function. Now remember that register function gets a plugin and that plugin we're going to call facify plugin and then the options is going to be plugin options. And we're going to have this function just return void. So we can see here that this, you know, the idea is this plugin interface is probably going to also require that plugin options. So let's make plugin options a generic for the register function. And we're going to pass it here. So this is one of my favorite tricks in TypeScript where we have a generic defined for this function register. But we've also specified it here as well as here. So let's write the facify plugin interface. We're going to export this one as well because we're going to use it in our whisper plugin definition file called facify plugin. We're going to, you know, it's also going to have that plugin options interface. And this interface is going to just return a function that function is going to contain the instance of facify and the options and it's going to return void. You'll notice that we're not doing anything with the server generic for this facify instance. And there is a reason for that. There's a longer reason, but the short version of it is when we have a plugin system like here, the plugin author won't know what type of instance is going to get passed here. It could be anything. It could be HTTP, HTTP2, HBS. If their plugin doesn't care what kind of instance it is, then neither does its type. And if its plugin does care what kind of type the server is, then we probably will have to handle that logic from within the plugin. And there is a longer conversation that we're currently having on, you know, how can we improve this sort of type to enable that extensibility. But going back to this plugin options generic. If we were to define an instance of this interface here and pass it this plugin options generic, it'll then pass that itself here as well. From the perspective of the register function, if we pass a type implementation of this interface to this plugin property, TypeScript will actually figure out that the plugin options generic that is defined in the instance of this interface is going to be the same as the one here. And you don't need to write it twice. So what does that all mean? We go to this whisper plugin definition file. And just like the previous one, we're going to export this constant variable whisper plugin, which refers to fastify plugin. And we're passing in that, you know, our options object here. In this case, it's secret string. Taking a look at that implementation. Now, when we import whisper plugin, you know, whisper plugin is uptype fastify plugin. And if you remember register takes in, you know, that first option is a fastify plugin. So it's able to take in and infer the generic plugin options based on the definition of our plugin. I know that can be a little confusing. So I hope this code snippet showed that. But the last point I want to make here is back to the whole declaration merging. When you have something like the whisper plugin that is, you know, adding a property to fastify. Let's go ahead and you know, while we're saying this change these comments over to the new implementation. And we can see that we are extending that fastify instance interface with that whisper secret. It shouldn't be the implementations job to do it. Instead, we can actually use the same declare block inside of the inside of the definition here. And we can achieve the same thing where let's say we just want to add that whisper secret property from our plugin. We go back over to our part three and we remove it from here. And notice that whisper secret is still of type string because by importing that plugin from, you know, from your module system, the definition file with it as well will be included. And because we're, you know, overriding the or merging the that property over the fastify instance interface, we get that same effect inside of our implementation. And with that, you've implemented implemented the basic plugin system for fastify. Welcome back. I hope you enjoyed that code, the code walkthrough. The as a reminder, the topics we covered today include discriminant unions function overloading name generic parameters declaration merging and generic inference. All of these are actively in use over in the actual fastify type definitions. And there's plenty of more information available on typescripts documentation to learn more about it. If you'd like to learn more about the types of definitions or even about fastify itself, come check us out and get up. We're always accepting of new contributors and contributions. And we are happy to include you in our community. Once again, my name is Ethan Ayrewood. I'm a software engineer at Microsoft. Thank you very much for listening to my talk today. You can find me on Twitter and GitHub and be happy to chat further about typescript, fastify or Node.js.