How to set up and use Mongoose with TypeScript

Updated: December 30, 2023 By: Guest Contributor Post a comment

Overview

Are you looking to leverage TypeScript’s strong-typing with Mongoose for MongoDB? TypeScript allows for easier development of complex MongoDB-based applications by catching errors at compile-time with static types. This tutorial aims to guide you through the process of setting up a TypeScript Node.js environment and integrating Mongoose, the popular object-modeling package for MongoDB.

Prerequisites:

  • Basic knowledge of TypeScript and Node.js
  • Node.js and npm installed on your machine
  • A MongoDB server available for connection

Setting up TypeScript

First, let’s set up a new TypeScript project:

mkdir mongoose-typescript-project
cd mongoose-typescript-project
npm init -y
tsc --init

This will create a new directory, initiate a new npm project, and generate a basic tsconfig.json file, configuring your project for TypeScript.

Let’s edit the tsconfig.json to suit our needs:

{
    "compilerOptions": {
        "target": "es2021",
        "module": "commonjs",
        "lib": ["esnext"],
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
}

Integrating Mongoose

Install Mongoose and its TypeScript definitions:

npm install mongoose
npm install -D @types/mongoose

Now, set up Mongoose within a simple Node.js application:

import mongoose from 'mongoose';

mongoose.connect('mongodb://localhost/test', {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

const db = mongoose.connection;
db.on('error', console.error.bind(console, 'MongoDB connection error:'));
db.once('open', function() {
    // we're connected!
    console.log('Connected to the database.');
});

Defining a Model with TypeScript

Let’s define a Mongoose schema for a User model with TypeScript interfaces:

import mongoose, { Schema, Document } from 'mongoose';

interface IUser extends Document {
    username: string;
    email: string;
    password: string;
}

const userSchema: Schema = new Schema({
    username: { type: String, required: true },
    email: { type: String, required: true, unique: true },
    password: { type: String, required: true }
});

const UserModel = mongoose.model('User', userSchema);

We just created a strongly-typed Mongoose model by using an interface that extends Mongoose’s Document.

Advanced Queries and Operations

Take advantage of TypeScript’s intellisense to perform operations safely:

const findUser = async (username: string) => {
    try {
        const result = await UserModel.findOne({ username }).exec();
        if (result?._id) {
            console.log(`User found: ${result?.toJSON()}`);
        } else {
            console.log('No user found');
        }
    } catch (error) {
        console.error('Error finding user:', error);
    }
};

findUser('johndoe').then(() => process.exit());

Utilizing Middlewares and Plugins

Use TypeScript to enhance Mongoose’s functionality with middlewares and plugins:

userSchema.pre('save', async function(next) {
    if (this.isModified('password')) {
        const hashed = await bcrypt.hash(this.password, 10);
        this.password = hashed;
    }
    next();
});

userSchema.plugin(somePluginAsAFunction);

Be sure that the plugins or middleware you create or use are compatible with TypeScript’s static type system.

Interfacing with Complex Data Types

Handle more complex data structures with deeper object models:

interface ILocation extends Document {
    city: string;
    country: string;
}

const locationSchema: Schema = new Schema({
    city: { type: String },
    country: { type: String }
});

userSchema.add({ location: locationSchema });

We add a nested schema to allow our user to have an associated location object.

Types & Hooks

Taking full advantage of TypeScript typings across Mongoose hooks is particularly powerful:

userSchema.post('save', (doc) => {
    console.log(`User ${doc.username} has been saved with id: ${doc._id});
});

This hook logs after a user is saved, with access to TypeScript typings ensuring an id property creation after saving.

Conclusion

Throughout this walkthrough, we seamlessly combined Mongoose’s schema-based approach to MongoDB with TypeScript’s powerful type system, creating a robust and maintainable codebase. From setting up the project to creating web-hooks, we harnessed TypeScript’s full potential to keep our data access layer type-safe, organized, and scalable. Implementing Mongoose with TypeScript invites fewer runtime errors and more developer productivity, leading to a superior development experience.