admin管理员组

文章数量:1302958

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!

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!

Share Improve this question asked Jan 25, 2021 at 15:07 NicolasNicolas 894 silver badges9 bronze badges
Add a ment  | 

5 Answers 5

Reset to default 4 +100

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.

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')

I know this question is a bit old, but here's a TypeScript helper to solve it. Hope it helps someone in the future

export const Category = modelHelper('Category', categorySchema, 'categories')
// modelHelper.ts

import {
  CompileModelOptions,
  InferSchemaType,
  Model,
  model,
  models,
  Schema,
} from "mongoose"

export function modelHelper<TSchema extends Schema = any>(
  name: string,
  schema?: TSchema,
  collection?: string,
  options?: CompileModelOptions
) {
  return (
    (models[name] as Model<InferSchemaType<typeof schema>>) ||
    model(name, schema, collection, options)
  )
}

for typesefyty just do

export type UserDocument = InferSchemaType<typeof userSchema>;

export const User = models['user'] as Model<UserDocument> || model('user', userSchema);

Another approach you could use is casting the mongoose.models.Category as never in a function so it is ignored. You then keep full type inference from your schema without having to redefine an interface elsewhere.

Using your example:

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

function createModel() {
  if (mongoose.models.Category) {
    return mongoose.models.Category as never;
  }
  return mongoose.model("category", categorySchema);
}

const Category = createModel();
export default Category;

// accessing retrieved properties is then inferred properly
const category = await Category.findOne();
category?.name;

本文标签: javascriptHow to properly use Mongoose models in NextjsStack Overflow