Good day guys, quite recently i made a post about setting up a nodejs with typescript, the article also talked about incorporating an express server plus mongodb and mongoose, incase you missed it you can read it here. In this article i am going to be focusing on utilizing the cool type system that come with TypeScript to build strongly typed mongoose models.
To read more articles like this please visit Netcreed
By the way what the heck is mongoose? In case you are not familiar with mongoose, it is a javascript library that allows one to define a schema for modeling our data when we are working with mongodb. Most people would say one of the drawbacks of working with mongodb is that you can't define how your data will look like i.e you don't explicitly define the schema of your data. Personally i don't see this as a drawback but it can lead to all sort of headaches if you are not careful, you don't want a collection to be having some documents that contain a field for age while other documents do not, or you might even store the wrong data type for a field, say a string where a number is expected.
Mongoose provides a solution for this by allowing us to define a schema for our data, this means that it allows us to define the structure of data so that all documents in a collection all have the same format to avoid inconsistencies in the collection. Mongoose also allows us to easily query our mongodb with it's set of already defined query functions and if you want something more granular you can extend the queries by defining your query helpers.
Installing Mongodb And Mongoose
I would suggest that you spend some time going through the official documentary to get more understanding about what you can do with mongoose. To use mongoose first you need to install mongoose, mongodb and the type definition for each respectively, you can see this article to learn how to install mongoose and mongodb with TypeScript. You have to ensure that you have TypeScript installed on the project because we are going to be utilizing the built in type system that comes along with TypeScript.
Connecting To A Mongodb Database
We need to connect to a mongodb database using mongoose, the code block below demonstrates how to go about that.
import * as mongoose from 'mongoose'
import * as express from 'express'
const app = express()
const url = 'your connection string to your mongodb database'
const PORT = 3000
mongoose.connect(url, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true})
.then(result => app.listen(process.env.PORT || PORT, () => console.log(`app running on port ${process.env.PORT || PORT}`)))
.catch(err => console.log(err))
app.get('/', (req: express.Request, res: express.Response) => {
res.send('<h1> Welcome </h1>')
res.end('<h3> We are building strongly typed mongoose models </h3>')
})
If you have basic understanding of express and mongodb then the above wouldn't be too much of a problem to understand and for simplicity's sake we will focus our attention on only mongoose and not express or how a node js server works. If everything goes according to plan and your server is up and running, you should see app running on port 3000
in your terminal.
Creating a Mongoose Schema
What the heck is a mongoose schema? A mongoose schema is basically an Object that will serve as the template from which we are going to create our Model. The model is just another name for a collection. Mongoose doesn't call them collections, they call them models while the schema is just the actual format that every document should look like. Well then let's create a schema;
import { Schema } from 'mongoose'
const heroSchema:Schema = new Schema({
name: {
type: String,
required: true
},
alias: {
type: String,
required: true
},
universe: {
type: String,
required: true
}
})
To create a Schema you need to import the Schema from mongoose, i destructured here to get the Schema but you could also do import mongoose from 'mongoose'
and then const heroSchema = new mongoose.Schema({})
. When we are creating a schema we pass in an object that has properties that will define the future structure of documents that will be a hero. We specified the data type that each field should hold using the type. This ensures that we can only store strings inside name field and so on and so forth. The required property ensures that we provide a value for this particular field when we are creating a new document to be added to the collection, if we don't it will throw off an error. We could also handle that error gracefully like the Queen of England. But i won't go into that here. Let's see how we can create a model and add a document to it
import { Schema, model } from 'mongoose'
const heroSchema:Schema = new Schema({
name: {
type: String,
required: true
},
alias: {
type: String,
required: true
},
universe: {
type: String,
required: true
}
})
const heroModel = model('hero', heroSchema)
function createHero = async function (heroDetails){
const hero = await heroModel.create(heroDetails)
return hero
}
const name = 'superman'
const alias = 'Clark Kent'
const universe = 'DCU'
const hero = createHero({ name, alias, universe })
And we have created our model but one thing, since we are working with TypeScript, naturally you would expect to see auto-completion and intellisence for the hero, but sadly we don't, and if we try to access a property on the current hero we get an error in our editor. This is because by default TypeScript will implicitly infer the Document type to our newly created hero. The hero has those properties we specified in the schema but TypeScript doesn't know that because by default the Document type doesn't have properties. How do we work around that? We need to create an interface that will extend from the Document interface, then we specify the contract on the interface and by contract i mean the values that any object that will implement our interface is supposed to have. We can now explicitly infer our schema and our model to be of that type. Here's a code example.
import { Schema, model,Document, Model } from 'mongoose'
// Interface for documents,
interface heroInterface extends Document {
name: string,
alias: string,
universe: string
}
// Interface for collections strong typing to heroInterface
interface heroModel extends Model<heroInterface> {
save(person: string): string
}
// Explicitly typing our user to
const heroSchema:Schema<heroInterface> = new Schema({
name: {
type: String,
required: true
},
alias: {
type: String,
required: true
},
universe: {
type: String,
required: true
}
})
const heroModel = model<heroInterface, heroModel>('hero', heroSchema)
// explicitly typing the hero model to be of our heroModel type
const createHero = async function (heroDetails):heroInterface {
const hero = await heroModel.create(heroDetails)
return hero
}
const name = 'superman'
const alias = 'Clark Kent'
const universe = 'DCU'
const hero = createHero({ name, alias, universe })
We have created an interface that extends from the Document class, this ensures that when we explicitly define the type for our heroSchema
we pass in the heroInterface
and we also ensure that the createHero function also returns a heroInterface
now we can access the fields on the hero like the name and we get auto-completion and intellisence. Likewise we also strongly type our model, when you create a model you can explicitly type that model to a Document interface and or a Model interface. This just means providing interfaces that extends from those interface we can tell the editor more about about the model or the document.;
- heroInterface`, so we get the all the fields on the document
-
heroModel
so we get access to all methods on the model itself.
When you create a new Schema, you can strongly type that schema to an interface that extends from the Document. Likewise models, we can strongly type a model to an interface that extends from the Model class. The beauty of this approach is that when working with a model or a document, you get access to the properties of the document and or instance/static methods defined on the model or the document.
That's for that, i hope you learnt something today and this was helpful to you in some form. Feel free to extend this with your means on strongly typing your mongoose models in the comment section.
To read more articles like this please visit Netcreed
Top comments (10)
Somewhere down the line,
userInterface
anduserModel
gets used instead ofheroInterface
andheroModel
. Is that intentional?Thanks for the heads up. It wasn't but I've made necessary corrections.
Good article, thanks for share it 👨💻
You are welcome man, glad that you liked it.
Thanks for sharing. I like to see how does typescript helps when different query patterns are invoked on mongoose model
You can definitely play around with it, i would be interested to know too
Personally I prefer Papr over Mongoose these days. It solves all the weird Typescript cases that you need to work around on Mongoose.
github.com/plexinc/papr
Maybe i should check it out.. Thanks for sharing
great, It's just a wow!
Glad that you liked it..