7

I'm building a Next.js application with TypeScript and MongoDB/Mongoose. I started running into an error when using Mongoose models, which was causing them to attempt an overwrite of the Model every time it was used.

Code causing Model Overwrite error:

import mongoose from 'mongoose';

const { Schema } = mongoose;

const categorySchema = new Schema({
  name: {type: String, required: true},
  color: {type: String, required: true}
})

export default mongoose.model('Category', categorySchema, 'categories')

I found that on many projects using Next.js and Mongoose, including the example project by Next.js, they used the following syntax on the export to fix this problem:

export default mongoose.models.Category || mongoose.model('Category', categorySchema, 'categories')

This feels pretty weird and "bandaid solution"-esque, but it seems to do the trick at first glance; it prevents an overwrite if the model already exists. However, using TypeScript I started running into another problem, which after some time I found was being caused by that very line. Since the export was finnicky, TypeScript couldn't parse the Category model, and threw errors whenever I tried to use most of its properties or methods. I looked deeper into it and found some other people going around this by doing:

import mongoose from 'mongoose';

const { Schema } = mongoose;

const categorySchema = new Schema({
  name: {type: String, required: true},
  color: {type: String, required: true}
})

interface CategoryFields {
  name: string,
  color: string
}

type CategoryDocument = mongoose.Document & CategoryFields

export default (mongoose.models.Category as mongoose.Model<CategoryDocument>) || mongoose.model('Category', categorySchema, 'categories')

Again, this seems to do the trick but it's merely tricking TypeScript into believing there's nothing weird going on, when in reality there is.

Is there no real solution to fix the Model Overwrite problem without jumping through hoops and covering errors with other errors?

Thanks in advance!

Nicolas
  • 87
  • 2
  • 8

2 Answers2

2

The key issue of this problem is that things inside mongoose.models are all typed as Model<any> by default.

So that type of

mongoose.models.Customer || mongoose.model("Customer", CustomerSchema)

will be inferred as Model<any> since this is a more broaden type.

The goal is to type mongoose.models.Customer correctly. And the type should be inferred instead of redefined.

Regarding your example, a solution can be like this:

const CustomerModel = mongoose.model('Customer', CustomerSchema)
// type `mongoose.models.Customer` same as `CustomerModel`
export const Customer = (mongoose.models.Customer as typeof CustomerModel) || CustomerModel; 

This does not require adding extra interface since it reused the model type you already defined.

ColaFanta
  • 1,010
  • 2
  • 8
  • All right, that seems to work perfectly! Only thing is, I thought the `mongoose.models.Customr || ...` part [was meant to](https://stackoverflow.com/a/66218711/5015356) avoid re-registering the model on file change. Doesn't the line `const CustomerModel = ...` cause OverwriteModelErrors? – Ruben Helsloot Jan 20 '23 at 21:53
  • I fixed it by creating a generic type from the `Schema`, based on [the mongoose type definitions](https://github.com/Automattic/mongoose/blob/master/types/index.d.ts#L76). You can see the code [here](https://github.com/rubenhelsloot/so-mongoose-type-example/blob/main/models.ts#L76) – Ruben Helsloot Jan 20 '23 at 22:05
  • It won't re-register because `mongoose.models.Customer || ...` means "get me registered model first if there is one, otherwise register one", it already ensures `CustomerModel` not to be called twice. And since `const CustomerModel=...` is not exported, nowhere else can invoke it. So it shouldn't cause `OverriteModelErrors`. – ColaFanta Jan 21 '23 at 01:08
  • Yes, creating a generic type same as mongoose type works. However, the issue is that there is no gaurantee that unexported mongoose type will not change in the future. You will have to keep an eye on the lib's change and recreating this generic type once the lib author modifies that type. That's why I prefer to infer type from `const CustomerModel` rather than creating one. – ColaFanta Jan 21 '23 at 01:33
0
export interface CategoryDoc extends Document {
    name:string,
    color:string
}

export interface CategoryModel extends Model<CategoryDoc> {}

then export it like this

// you might remove the 'categories'
export default mongoose.models.Category || mongoose.model<CategoryDoc,CategoryModel>('Category', categorySchema, 'categories')
Yilmaz
  • 35,338
  • 10
  • 157
  • 202
  • To be clear, I'd want to avoid having to write a separate interface and schema, because it means keeping the fields in two different places. Also, this would mean that more complex properties, like static methods, are lost – Ruben Helsloot Jan 15 '23 at 17:20
  • @RubenHelsloot Can you pls create a reproducible code base or share your repo? – Yilmaz Jan 15 '23 at 18:26
  • 1
    sure! https://github.com/rubenhelsloot/so-mongoose-type-example – Ruben Helsloot Jan 17 '23 at 11:10