How Add TypeScript Types to a MongoDB Database
Though we've been able to make our database connection from the server, we haven't appropriately specified the type of data that can be returned from our database collections. In this lesson, we introduce and take advantage of generics to define the type of data that can be returned from our listings collection.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL with a single-time purchase.

[00:00 - 00:09] Let's bring the benefit of TypeScript to our Mongo functionalities. What we're going to aim to do is create type definitions for our database collections.
[00:10 - 00:27] We'll do this in a new file called types.ts that lives under a lib, or that is to say library folder in our source folder. This types file is where we'll keep any type definitions that are to be used in our code in multiple different areas.
[00:28 - 00:40] For now, our database only consists of a single listings collection. Let's look to create an interface that is going to resemble the shape of a document in our listings collection.
[00:41 - 00:59] This will look very similar to what we had before when we created an interface for our mock listings array. In a MongoDB document, the ID field is a little unique.
[01:00 - 01:08] First, it's prefixed with underscore, so it's actually underscore ID. In addition, it actually has a pretty unique type.
[01:09 - 01:27] In Mongo, the underscore ID field is a unique 12 byte identifier, which by default is usually generated by Mongo as the primary key for a document in a collection. This object ID plays a role in how documents can be searched, filtered, and even sorted in a collection.
[01:28 - 01:51] With that said, we'll type define this underscore ID field with the appropriate object ID type provided to us from the type definitions file of the node-longo driver. Let's now try to create an interface that will help shape the object being returned from our Connect database function.
[01:52 - 02:04] We'll establish this as a new interface called database. For now, our database in Mongo only consists of a single listings collection.
[02:05 - 02:24] To represent the shape of a collection, the Mongo type definition file provides an interface called collection. Since our database contains a single listing collection, we can specify that in our database interface and provide a value of the collection interface.
[02:25 - 02:47] Now, when we actually go back to the source index file and hover over the listings property in database, we can see that the type of listings is already recognized as a collection, so we haven't done anything to improve that yet. The collection interface is a generic and actually accepts a type variable.
[02:48 - 03:03] Here we can add the listing interface we've created as the type parameter of this collection interface. Let's take a quick tangent now and talk about TypeScript generics, since this might be a very good time to.
[03:04 - 03:20] TypeScript generics is one piece of TypeScript that often confuses newcomers since it makes TypeScript code appear a lot more complicated. We'll spend the next few minutes going over an example shown in the TypeScript documentation to better illustrate this.
[03:21 - 03:45] Now, first and foremost, generics is a tool that exists in languages like C# and Java to help create reusable components that can work with a variety of different types. TypeScript follows this pattern by allowing us to create code that can work with different types, and it does so by allowing us to abstract the types used in functions or variables.
[03:46 - 03:58] Here's a very simple example extrapolated from the TypeScript documentation. Assume we had a function called identity that receives an argument and returns the said argument.
[03:59 - 04:22] Since it's expected to return what it receives, we can be explicit and specify the type of the argument and the value returned by the function to be the same, for example we can say number. If we try to change the return type of the function, TypeScript will complain and rightly so because it recognizes the parameter is being returned.
[04:23 - 04:33] If we were to run this function and pass in a number, the number will be returned. Now, what if we wanted this function to be reusable for different types?
[04:34 - 04:48] That's a totally valid case since we might want this function to be used for anything, a number, a string, an object, etc. One thing we could try to do is specify union types, where the return type could be one of many.
[04:49 - 05:05] For example, we can say the function argument can accept either a number or a string and can return either a number or a string. But this might not be very reusable, especially if we don't know what type we 're going to be passing in.
[05:06 - 05:21] Another approach we could take that could work for any and all types is to use the any keyword. But this isn't really good since it won't tell us what the function is going to return.
[05:22 - 05:36] Here is where generics and the capability of passing a type variable comes in. Just like how we said this identity function accepts an argument, we can say the identity function is to accept a type variable as well.
[05:37 - 05:48] And this is denoted by using angle brackets in TypeScript. Here we're using the letter T as the name of the type variable of our identity function being passed in.
[05:49 - 06:02] Just like how the value argument is available in the function, the type argument is also available in the function. We could say that whatever type variable is being passed in will be the type of the argument and the return type of the function.
[06:03 - 06:19] Notice that in where we're actually running our functions, even though we haven 't passed in a type value, the TypeScript compiler is smart enough to pick up what these types are. However, in more complicated cases, we'll need to pass in the type variable as well.
[06:20 - 06:28] Generics can be used pretty extensively. For a random example, assume we wanted to create an object that has the argument option within.
[06:29 - 06:38] Also assume we wanted to type constraint this object with an interface. Types and interfaces also accept type variables.
[06:39 - 06:59] So we could define an identity object interface outside of the function, say that it accepts a type variable and set the field type as that variable. In our function, we can then define the type of the object we're creating as that interface we've just set up and pass the type variable along.
[07:00 - 07:17] And we can just simply return the field from the object as well. Just like how we can assign default values to function parameters in ES6 JavaScript, we can assign default values to type variables as well.
[07:18 - 07:39] In a lot of third party libraries where they declare interfaces and types to be used, you may notice a default type variable usually be set to any. Now if a type variable isn't defined and the compiler isn't able to infer what the type variable might be, it will simply set it to any.
[07:40 - 08:03] The letter T is often used to infer a type variable by convention and is most likely due to the fact that it probably stands for type. We could very well use any letter we want, like U or V, and sometimes you may find it easier to extrapolate the name of the type variable, especially if we pass in multiple type variables to a function.
[08:04 - 08:27] Notice how much more complicated and scary this code now looks? It's actually not that difficult, especially once you recognize what's really happening and if you're able to trace things slowly step by step.
[08:28 - 08:51] Okay, we'll stop here for now. We're going to remove this identity function and look back at the collection interface we've set up for the listings collection. As an interesting note though, when we move to the front end code, we're actually going to create our own functions and we're actually going to implement and use generics quite a bit so we'll get a good revision of what we 've just said when we get to that point.
[08:52 - 09:14] Why is the passing of the listing interface helpful in the context of our collection interface? This might have been mentioned before, but if not, one amazing capability that TypeScript provides with working with third party libraries is the capability to inspect and see the type definitions of the declaration file for these libraries themselves.
[09:15 - 09:26] With VS Code, we can navigate to the collection interface itself by command clicking. We won't spend a lot of time here, but we'll quickly summarize what is sort of being done here.
[09:27 - 09:41] We can see that the collection interface takes a type variable labeled TSCima and it sets it to a default value called default. When we hover over the default type variable, we can see that it is of type any .
[09:42 - 09:54] When we do a very quick search of TSCima, we can see some functions that utilize it in such a way that the return statement of the function is essentially the type we've passed in. Amazing.
[09:55 - 10:13] Let's close this file and go back to the types file we've created. So when we pass into the listing interface to collection, we want to help ensure that many of the Mongol methods we intend to use, such as find or find one, they would return the listing type from their results.
[10:14 - 10:24] So then our TypeScript code would recognize the type of information being returned from our database queries. Let's put this to the test.
[10:25 - 10:47] We'll head over to the database/index file, import the database interface from the types file in the lib folder, and look to apply it as the return statements of our connect database function. Our connect database function is asynchronous, so we can't simply specify the return type of the value we expect.
[10:48 - 11:01] We have to say it's going to be a promise of this return type. Well, TypeScript natively provides a promise interface, which accepts a type variable to do just this.
[11:02 - 11:18] Essentially, we're saying our connect database function will return a promise that when resolved will be an object of type database. Our listings field in the object being returned will now be inferred to be a collection of listing.
[11:19 - 11:51] As a side note, we could have very well specified the type variable here directly in this collection function, but we've achieved the same thing by explicitly specifying the type of the whole function, the type of the whole return statement of the function itself. Now, when we head to the source index file and survey the listings value from our Mongo Find function, we can see that the listings variable we're setting up is appropriately type defined as an array of items that resemble the listing interface.
[11:52 - 12:13] This helps a lot, since if we wanted to access a property that doesn't really exist, TypeScript will emit a warning. Since we haven't made any functional changes, when we start the server again, our server should work as expected and console log the sample listings data in our collection.
[12:14 - 12:31] Amazing. Since we don't need to verify that our database connection works any longer, let's remove the check and console log we have for the listing sample data in our mount function. We need to get into the next step.
[12:32 - 12:40] We need to get into the next step.