Typescript With MongoDB and Node/Express | by Ayobami Adelakun | The Startup | Medium

Typescript With MongoDB and Node/Express

Ayobami Adelakun

The Startup

Ayobami Adelakun

·

Follow

Published in

The Startup

·

·

Aug 17, 2020

18 min read

Typescript is a superset of javascript. when we are writing typescript we are still writing javascript, except in this case we are writing javascript with a type system. So, in Summary, Typescript is Javascript plus type checking. The final source code for this tutorial can be found HERE

Why Typescript?

Typescript introduced a type system to javascript and these are the advantages it gives us during development.

  1. Typescript can help us catch errors during development
  2. Typescript makes our Javascript code easier to read and debug
  3. Typescript is open source.

There are many more reasons to use typescript, and you can find out more about that in this article: https://dzone.com/articles/what-is-typescript-and-why-use-it

This tutorial assumes you already have some knowledge of Node.js and you want to know how to use typescript with Node.js

Also, I Recommend VScode as a code editor because VSCode includes typescript language support.

Getting Started

Create a new folder for your app, you can call it anything, on your terminal navigate to the newly created folder and run this command

npm init -y

this will create a package.json file for us.

install the following dependencies

npm install typescript --save-dev

npm install express body-parser --save

The next thing is to create a tsconfig file for typescript,

run this command on the terminal

npx tsc --init

if you are having issues with this or this is throwing an error, install typescript globally like this:

npm install typescript -g

then run the command again,

This will create the tsconfig file for us, tsconfig specifies the root folder and the compiler option to compile our typescript code down to javascript. we can always edit this file to modify the default configuration, but we’ll go with the default configuration in this tutorial.

Now that we have installed typescript and we have our configuration file, let’s get started with creating our express server

Setup the server

create a new folder and call it src, in our src folder create a new file index.ts, open the file and add the following code:

You should notice your code editor flagging your code, hover on the express module and you should see something like this:

This is because we do not have a type definition file for our express package, typescript analyzes our code for errors. When we install and import a third-party module, typescript doesn’t know the type required for this module, we need to install a type definition file to inform typescript about the types, arguments, and return types of this package. The type definition file always ends in .d.ts, in the case of our express package, for instance, we need to install the @types/express module to inform typescript about the types and other information required by our express package, let’s do that now

npm install @types/express --save-dev

You will notice that as soon as we installed this package the error goes away, this is because typescript now has access to the type definition file, and now it can use this file to check our code for errors and the types required when we work with the express package.

Also, Notice that we are not using commonjs require function, instead, we are using the import statement to import our code, typescript prefers to use the import statement over the require statement, otherwise, we’ll see an error thrown, you can try this on your own.

Now, in our package.json file, let’s create our start script, add the following

Note that Node cannot run our typescript code directly, so we have to compile it down to Javascript, but with the help of a package ts-node we can run typescript directly with node, but we are going to use ts-node-dev instead because it will watch our files for any changes and restart the server, just like nodemon. let’s install this package

npm install ts-node-dev --save-dev

Now let’s update our start script to use this package

Now let’s start our server

npm start

If you see something like this, then we are good to go.

server is listening on port 3000

Setup our routes

In the src folder, create a new folder and call it routes, inside the routes folder, create a new file and call it todo.ts, include the following code

Now, back in our index.ts file, let’s register the route. Add the following code to import the todoRouter.t

Then register it in our express app like this:

In route/todo.ts, hover on the req and res parameter, you will notice that req has an implicit type of Request and res has an implicit type of Response. if you pass in a middleware, say, for instance, an empty array and you hover on the req and res property again, you should see that they have an implicit type of any.

See line 5 for the updated code

The implicit type of any means we can assign values of any data type to our req and res property and typescript will be fine with it. we do not want this in our application, we want typescript to tell us or catch this error whenever we knowingly or unknowingly try to assign values of any type, be it string, number, etc to req and res property. we want the req and res property to only be of type express.Request and type express.Response respectively. To do this we have to import the types from our express app. in todo.ts file. include the following code in routes/todo.ts.

Now, let’s include the Request and Response types to our route function parameters, in the same file, include the following code:

okay, Now let’s test this out with postman

make a request to the get endpoint

localhost:3000/api/todo

if you get the same response as me, then we are good to go.

Configuring Mongoose for data persistence

There are a few issues when using typescript with mongoose, we are going to discuss them in this tutorial and the fix to make typescript work well with mongoose.

The first issue is around creating a new document. say, for instance, we created a new mongoose model and we want to create a new instance of that model, assume the model is Todo, we’ll create a new instance of the model like this:

new Todo({title: 'new title', description: 'some description'})

As you can see, the title is a type of string, the description is also a type of string, if we assign a number to the title field while we expect it to be a string, typescript will not catch this error, and we’ll not be notified, which goes against one of the reasons why we are using typescript in the first place, which is to catch errors during development.

Also, we can add a new property that is not declared in our schema to the model instance above and typescript will be fine, we can make mistakes like this if we have a typo while typing the property of the model instance, say for instance, instead of title , we typed titl like this:

new Todo({tit: 'new title', description: 'some description'})

Typescript will not catch this error and we’ll not be notified, also if we include an extra property like this:

new Todo({title: 'new title', description: 'some description', hour: 3 })

Typescript will also not catch this error, although we do not require the property “hour” from our schema definition.

Also, another issue arises when we create a new instance of our mongoose model like this:

const todo = new Todo({title: 'new title', description: 'some description'})

and we console.log Todo like this:

console.log(todo)

mongoose may return something like this

{title: 'new title', description: 'some description', createdAt: '', updatedAt: ''}

As you can see from the result, mongoose returns additional properties(createdAt, updatedAt) which typescript may not be aware of, we need a wat to tell typescript that there may be some additional properties that get created behind the scenes by mongoose when we create a new instance of our model. We’ll be looking at these issues and creating a fix for them, but before continue, let’s connect our application to the database first.

Connect to MongoDB

Please make sure you have MongoDB installed on your computer before proceeding further.

Install mongoose,

npm install mongoose

Now let’s import mongoose in our index.ts file, add the following code

Typescript is flagging our mongoose package. if we hover over it we’ll see that typescript is telling us that it cannot find a file declaration for mongoose module. As discussed earlier on, we need to install type definitions for all our installed packages, just like we did with express. for mongoose run this to install the type definitions.

npm install @types/mongoose --save-dev

You shouldn’t see the error again after this installation.

Now let’s connect our application to a database.

run npm start again, if you see this on your terminal then we are good to go:

connected to database

Make typescript work with mongoose

In our src folder, create a new folder and call it models.

mkdir src/models

In the models folder, create a new file and call it todo.ts, open the file and include the following code:d

Now lets’s create different instances of our model the wrong way and watch as typescript allow us to go ahead with this:

Please read the comment in the code for more information on the wrong thing we are doing in these lines of codes.

The first issue: make typescript aware of the properties and data types that we expect in our model instance.

To solve this we are going to be making use of an interface.

what is an interface ?

An interface is a syntactical contract that an entity should conform to. In other words, an interface defines the syntax that any entity must adhere to(definition from tutorialspoint). this means that an interface defines the shape of an object, the properties it should have, the type of the properties, and functions.

For our Todo, the schema contains title which is a string, and description which is a string. we are going to help typescript to understand this by defining an interface for this model, by so doing, typescript will be aware of the properties that our todo expects.

in models/todo.ts include the following codes:

The above code is how we create an interface in typescript, the ITodo is a contract which defines the properties and the data types that are required in our Todo. We have only defined this interface, we are not using it yet.

Now, to make use of this interface, we would create some little trick for creating an instance of our model, instead of creating our model instances directly by writing

new Todo({ title: 'some title', description: 'some description' })

we would define this instance in a function, this function will take the new todo object as a parameter,

Note: We make use of an interface the same way we define types for properties and functions. so if I have a function that takes a parameter called name which should be a type of string, I’ll do something like this:

function test(name: string) {}

As you can see, name is a parameter, and it should be a string according to the function above, the same way, if I am to use my todo Interface in this function on the name parameter then the function will look like this:

function test(name: ITodo) {}

This means our name parameter is expecting an argument which must be an object that contains a property title, which must be a string, and description property which must also be a string, remember these are properties listed in our ITodo interface.

interface ITodo {

title: string;

description: string;

}

Anything other than this typescript will show an error.

Now let’s create the mystery function that we have been talking about. we’ll call the function “build”, and we’ll call this function every time we want to create a new instance of our Todo. in our model/todo.ts, include the following code:

As you can see, we have a build function, which has a parameter attr, attr is a parameter of type ITodo, Remember we defined ITodo as an interface above, this means attr expects an object which must contain a title property which must be a string and a description property which must also be a string. this parameter is what we passed into our Todo Model to create a new instance of this model. Now typescript is aware of the object that must be passed into our model instance and will throw an error if something else is provided.

let’s test this out, if I call the build function and pass in wrong property values as we did above, typescript will throw an error this time around, in our model/todo.ts, include the following code:

As you can see, we have typescript flagging our code this time around as we attempt to create a new instance of our Todo model with wrong information using the build functions.

hover on the first one and you will see that typescript is trying to tell us that titl, cannot be assigned to our parameter because our ITodo interface does not have it defined. Now we have been able to make typescript aware of the type and properties of our model instance arguments

We can still make some slight improvement to our code, how do we do this, instead of having to call our build function directly, we can make the function into our Todo model so we can do something like this:

new Todo.build({
title: '',

description: ''

})

This will be better as we will only export our Todo Model being confident that we have the build function to create a new instance of our Todo model

Now how do we do this ?

To do this, we are going to attach the build function to the statics property of our todoSchema, like this:

Now we can call build directly on our Todo model, but we are going to encounter a problem when we attempt to use this, let’s see:

As you can see, we have typescript flagging our build function, this is because typescript does not recognise this code below as a valid code

todoSchema.statics.build = (attr: ITodo) => {

return new Todo(attr)

}

this is yet another weird thing with typescript and mongoose, typescript does not understand what it means to assign a property to statics object of the todoSchema, although this is valid javascript, typescript does not understand it. to solve this, we have to give a little more information to typescript, and we are going to do that by defining the properties that our Todo model will have in an interface. This interface will tell typescript that we will have a build function in the Todo model. in our models/todo.ts file, include the following code:

Notice that we are defining our interface a little differently here, this is because we just want to add the build function to our existing Todo model, to do that, we are extending the existing model interface, the extend keyword means, “take all properties of the existing mongoose.Model interface, add them to our new interface, TodoModelInterface”, then we include the build function as part of the properties in the TodoModelInterface, now the TodoModelInterface contains all the properties of our mongoos.Model interface and the build function

Also, Notice the angle bracket <any>, I am not going to go deep into the details of this, this is called generics in typescript, you can read more about it, but imagine it like a function definition that takes in a parameter and a function call that takes in an argument, in this case, we are calling the mongoose.Model interface and it needs additional information(the argument we passed in) that will be used in the interface body(mongoos.Model interface), the argument we are passing, in this case, is a type(generics take in types as arguments), and it is a type of “any”, we’ll be changing this later, but let’s see the reason why we are passing in the “any” type for now.

Note: Generic take in types as arguments

Hover on the Model property and press command and then click, this will take you to the type definition file for this interface,

You will see that the first generic parameter is T and it extends Document, since we don’t have our mongoose document type defined yet we’ll use type any, for now, remember I told you to imagine generics like a function declaration and function call, imagining it this way will help us understand what we are attempting to do in this tutorial.

In our TodoModelInterface, notice how we defined the build function, we defined the function, the parameter and the data type we expect, in this case, our custom data type of ITodo, then we also specify the type of data we want the function to return, in this case, we want the function to return a data of type any, we will also be updating this later. Type any means the data can be of any type.

To use this newly created model interface, we will go to the part of our code where we created our model, I am talking about here:

If you are on Mac, press command then click on model, For windows users, try CTRL instead(if it doesn’t work please check on google). This should take you to the type definition file for that model function, you should see something like this:

Check line 292 in the screenshot above, you will see that we have a function model, I told you to imagine generics as a function, you will see that we have an angle bracket in front of the function model, these are generic properties, but imagine them as function parameters, as you can see the first one is T and it extends Document, this means whatever we pass to our model function as the first generic argument must be a mongoose document, since we have not defined this yet, we’ll pass in type any, for now. Take a look at the second parameter, you will see that it extends Model, that means that the second generic argument we’ll be passing to our model function must be a mongoose model, and we have our todoModelInterface defined already, so we’ll pass that in, let’s do that now

as soon as we do that, over on our code

Todo.build({})

You will see that the initial error has gone, this is because we now have the build function defined in our model interface for our Todo model. if you see an error now, it will only be that we should pass in the required property of title and description to the object passed to the build function. let’s do that:

As soon as we did this you can see that the error is completely gone

Now this works fine, and this solves two of our problems that relate to properties and data types.

Now moving to the last issue, we mentioned earlier on in this tutorial that mongoose returns additional information when we create a new Document in the database, and we gave the example of some properties that may be added to our document, and they include createdAt, and updatedAt.

Also, recall that we passed in a type of any to our extended mongoose.Model interface, from our inspection we realised that this should be a mongoose Document but since we did not have it defined at the moment we passed in a type of any, also notice that our build function returns a type of any. All things being equal, our build function creates a new instance of our model and returns a new document, but since we do not have a new document defined yet, we used the type of any as a placeholder.

What we need to do now is to define an interface for our documents, this interface will define the properties that we want to be contained in our Todo document or that we expect to be returned when we create a new document. Let’s do that, in the same file routes/todo.ts, type this code:

Like the explanation we gave above, we are extending the existing Document interface, the extend keyword means, “take all properties of the existing mongoose.Document interface, add them to our new interface TodoDoc”, then we include some other properties such as title and description as part of the properties in the TodoDoc, now the TodoDoc contains all the properties of our mongoos.Document interface and title, which is a string, and description which is also a string, if mongoose adds extra properties such as createdAt and updatedAt, then we’ll define them in the TodoDoc interface like we defined title and description. but in our case, these extra properties are not added

Now to apply this interface, we’ll go to the part of our code where are creating our model, remember the first generic argument is a type of any, from our inspection earlier on we realised it should be a document, but since we didn’t have a document interface defined yet we decided to use the type any, now we can use the TodoDoc instead of type any

Also, in our TodoModelInterface, our build function should return a document, but we are returning a type of any, now we can update it to use the TodoDoc interface

In the same TodoModelInterface, you will realise that the mongoose.Model also has a generic argument of type any, from our inspection we realised that this should also be a mongoose Document, let’s update it now to use our TodoDoc

Now let’s use our Todo Model in our route files,

in models/todo.ts, export the todo model like this:

in our route/todo.ts, import the Todo model like this:

Now, let’s create a todo, remember we have a post function for creating todo, let’s update that code, type the following update in router.post

You can test this out by yourself on postman.

Also, let’s update our router.get funcrtion, type the following code:

I tested these routes out on postman and it all worked fine for me, hopefully, it works for you too.

This brings us to the end of this tutorial.

In this tutorial we have learnt the following:

  • what typescript is
  • why we use typescript
  • how to use typescript with Node/express
  • Issues with using typescript and Mongoose and the fixes for them.
  • What interfaces are
  • how to declare an interface
  • how to add types to function, variables, parameters
  • What generics are
  • how to add generic types to functions

I’m open to suggestions and corrections on this tutorial, For the more experienced ones, If there is anything you think I didn’t explain quite well or any contribution you will like to make to this tutorial to make it better, kindly drop your comments in the comment section, I’ll be glad to learn from you too, and the rest of us reading this article would also have an opportunity to learn from your experience.

If you have learnt anything from this tutorial, please follow me on TWITTER, for notifications when I post new articles. Also, if you do not learn anything new and you think I deserve a follow, kindly follow on TWITTER 😀😊.

I’ll be glad if you follow me on medium too 😁. just hit the follow button at the top of the article.

I have the code to this article on github, you can fork it here, and you can follow me too.

Thank you for your time spent going through this article, I really hope I have helped someone to learn a few things about typescript and how to get started using it with Node.js/Express and Mongoose

Cheers! 💖💖