As a Node developer, chances are that you've already encountered with both Mongoose, and ODM (Object Document Mapper)library helping you connect your app to a MongoDB and TypeScript which is a typed superset of JavaScript that enables the developer to write code with less potential bugs, increased maintainability and smarter code completion through types.

Because both technologies are so popular, it is not a surprise people want to use them together.

A common problem however when using Mongoose with TypeScript is that you have to define both the Mongoose model and the TypeScript interface which leads to redundancy.


State of Software Develoopment Report

interface User {
  name?: string;
  age: number;
  job?: Job;
  car: Car | string;
}

interface Job {
  title?: string;
  position?: string;
}

interface Car {
  model?: string;
}

mongoose.model('User', {
  name: String,
  age: { type: Number, required: true },
  job: {
    title: String;
    position: String;
  },
  car: { type: Schema.Types.ObjectId, ref: 'Car' }
});

mongoose.model('Car', {
  model: string,
});

If the model changes, you also have to keep the TypeScript interface file in sync or the TypeScript interface would not represent the real data structure of the Mongoose model.

For a small application this may not be a huge problem, but as your system grows syncing becomes harder, developers tend to forget it, which creates nasty bugs.

Typegoose aims to solve this problem by defining only a TypeScript interface (class) which needs to be enhanced with special Typegoose decorators.

class User extends Typegoose {
  @prop()
  name?: string;

  @prop({ required: true })
  age: number;

  @prop()
  job?: Job;

  @prop({ ref: Car, required: true })
  car: Ref<Car>;
}

class Job extends Typegoose {
  @prop()
  title?: string;

  @prop()
  position?: string;
}

class Car extends Typegoose {
  @prop()
  model?: string;
}

Under the hood it uses the reflect-metadata API to retrieve the types of the properties, so redundancy can be significantly reduced.

Usage

To get started, create a Node project and add TypeScript and Typegoose as dependencies.

npm init
npm install -D typescript
npm install -S typegoose
./node_modules/.bin/tsc --init

IMPORTANT! The generated tsconfig.json must be edited so Typegoose can properly work. Both the experimentalDecorators and emitDecoratorMetadata must be set totrue.

After that create a Mongoose connection to your MongoDB instance.

import * as mongoose from 'mongoose';

mongoose.connect('mongodb://localhost:27017/test');

Then you can define your own Mongoose models using Typegoose.

import * as mongoose from 'mongoose';
import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';

class User extends Typegoose {
  @prop()
  name?: string;
}

const UserModel = new User().getModelForClass(User);

For the full API documentation check out the project's API section.

Now that the Mongoose connection is established and a model is defined using Typegoose we are now able to use the model object to manipulate the database.

import * as mongoose from 'mongoose';
import { prop, Typegoose, ModelType, InstanceType } from 'typegoose';

mongoose.connect('mongodb://localhost:27017/typegoosetest');

class User extends Typegoose {
  @prop()
  name?: string;
}

const UserModel = new User().getModelForClass(User);

(async () => {
  const u = new UserModel({ name: 'JohnDoe' });
  await u.save();
  const user = await UserModel.findOne();
  console.log(user); // { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
})();

And there we have it! A working example of Typegoose in action.

The real benefit however comes during development time, when Typegoose tells typing information to TypeScript about the decorated Mongoose model. This repels several potential bugs as well as helps the developer through the auto complete feature.

Typegoose1

Here Typegoose helps the developer by showing the property on the Mongoose model.

Typegoose2

In this scenario if we define a static class method, which equals a static in terms of a Mongoose model, we can see that TypeScript knows about this model function and helps correcting the typo we made.

Conclusion

In this post we've tackled the redundancy issue which is present when you use Mongoose with TypeScript. Instead of defining the Mongoose model and the TypeScript definition file separately, we can use Typegoose to create a TypeScript class which encapsulates the Mongoose model while giving typing information about the underlying model.

Please, let me know how this works for you.


State of Software Develoopment Report