Mongoose with TypeScript

Mongoose

When I started to work with .NET Videos website, I was curious how will code sharing be easy in the JavaScript application, as both server and client side share the same language and it would be cool to keep one code and contracts for the same entities on both sides. As I already blogged before, I needed to start with moving my server-side code to typescript as the starter project I used was handling server-side in javascript and client-side in typescript, which I found ugly.

You want to keep your data-model defined well and control it easily, knowing that MongoDB doesn’t really keep any schema. This means that if you want to insert into your table some piece of data that you invented after few beers, nothing will prevent you from doing that, if you won’t take care of it yourself (hopefully before taking a beer). To be fully honest with you, my data model for now is quite ‘short’ and it’s far from being well-defined. However, what I wanted to keep straight from the beginning is that the contract must be defined and ‘something’ must watch the data to meet those constraints as soon as possible.

So I started with definition of an Interface for a video, which would serve as a schema for my database table (or Collection, in MongoDB). I wanted to rely on it also on client side, as I want to be able to validate stuff as early as possible and possibly share the logic that will perform this validation between client and the server. I started with IVideo interface, which was extending mongoose.Document in order to be able to build a mongo model based on that. So the code would be something like:

// Create a `schema` for the `video` object
interface IVideo extends mongoose.Document
{
  //mongoose id?
 // _id?: any;
 //id for the entity
 id?: string;
 //each video has some title which is displayed first ot the user
 title: string;
 //each video must have an Url, whatever the place of publication
 url: string;
 //each video will have also it's url on dotnetvideos website
 localUrl: string;
 //code defines the unique identifier of a movie within a given website; usually it's a part of URL
 code: string;
...
}

But there are quite a few things that are wrong here.

  • This interface is defined in the place where you define your mongo models, so on server side, and I want to reuse its definition on the client side, so I would need to export it also to the client.

I didn’t like this so I have exported IVideo interface to some ./shared/ directory so that both server, and client side refer to the shared piece of code explicitly.

  • Second thing is that even if I export it to client, I am also exporting mongoose.Document, which is not intended – why would I want to have some database internals on the client-side code.

This time I have removed from my shared directory the part related to mongoose. I removed the ‘extends’ part and defined a new interface called IVideoModel on the server-side that extends both mongoose.Document AND my IVideo, without having much more to do actually.

I had to define also the Schema of the table in order to build a model on top of that. I find this part a bit redundant knowing that my interface is supposed to protect that the objects that I am using here are following some constraints. This is a bit misleading, however, as the interface I defined lives in TypeScript. This means that it doesn’t do much on the JavaScript side (TypeScript interfaces transpile to … nothing, as there’s no such notion in ES5 and ES6/2015). So there’s nothing also in the runtime and we do want to keep some consistency-check there as well.

Now I was already able to write something like the code below and have the mongoose model interface following the same interface I use on the client-side, without having to use mongoose stuff there.

// Create a `schema` for the `video` object
interface IVideoModel extends mongoose.Document, IVideo {}

let VideoSchema = new mongoose.Schema({
 title: { type : String },
 url: { type : String}, 
...
});

// Expose the model so that it can be imported and used in
// the controller (to search, delete, etc.)
export default mongoose.model<IVideoModel>('Video', VideoSchema);

So now, I use IVideoModel on server-side and IVideo on client-side.

  • Next thing is about the first comments from my interface definition. I want to be able to refer to the identity of my videos on the client-side, and the _id property of MongoDb document, which it is adding automatically, is of type ‘ObjectId’. I can imagine having these ids in some urls or something being very much a text values rather than some ‘ObjectId’. So in my interface I first defined
//mongoose id?
 _id: string;

which was leading to such error in IVideoModel:

Interface 'IVideoModel' cannot simultaneously extend types 'Document' and 'IVideo'. Named property '_id' of types 'Document' and 'IVideo' are not identical.

That’s precisely because _id from my interface is of string type, not ObjectId. What a shame. What to do now? Change it to ObjectId in the interface and cast it upon retrieval? Or add new property to the interface? I tried to do it and added

//id for the entity
id?: string;

I even find on the web, that Mongoose is supposed to generate some ‘id’ property to make it accessible with string type. The thing is that it didn’t work when I used it ‘as is’. Finally I came up across mongoose virtuals which actually let you define on the schema level properties that will be generated for you. The idea is to remove the _id from interface and have a field ‘id?: string; in both my interface and my schema that will be filled automatically and serialized when I need it to be sent between the server and the client.

// Duplicate the ID field.
VideoSchema.virtual('id').get(function(){
return this._id.toHexString();
});

// Ensure virtual fields are serialised.
VideoSchema.set('toJSON', {
virtuals: true
});
  • Last thing I needed to handle, is how to get my mongoose.Schema automatically updated when I add something to my interface. The problem is that mongoose.Schema is a class. When you’re creating the model, you need to instantiate the schema (this is the second argument passed to the mongoose.model function). So what I would need is to have mongoose.Schema automatically implement my IVideo interface when it changes. If I leave it like that, when I add anything to IVideo interface, Schema remains unchanged without any errors during compilation, and since mongoDb actually lacks schemas, it will allow you to save anything you like, unless you are aware of it and you will pretend it from doing so.

The solution I came up with so far is to create a class definition for the schema, that will extend mongoose.Schema AND implement my IVideo interface. This doesn’t solve every problem as I still need to manually add new properties to the implementation, but at least it will generate a compile-time error if I forget about adding it.

Everything is of course available on github

Feel free to comment if you feel what I’m saing doesn’t make sense and you have some good experience working such things out, I’d be happy to hear from you!

2 thoughts on “Mongoose with TypeScript

    • Is the issue you are mentioning, that you can’t update your mongoose schema when your data abstraction changes? The dirty-hack solution I had was to make gulp to create .js schema the way I want, but there are probably much more clean solutions out there. Let me know if that’s what is bothering you. I will find some better way 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *