127

Trying to implement a Mongoose model in Typescript. Scouring the Google has revealed only a hybrid approach (combining JS and TS). How would one go about implementing the User class, on my rather naive approach, without the JS?

Want to be able to IUserModel without the baggage.

import {IUser} from './user.ts';
import {Document, Schema, Model} from 'mongoose';

// mixing in a couple of interfaces
interface IUserDocument extends IUser,  Document {}

// mongoose, why oh why '[String]' 
// TODO: investigate out why mongoose needs its own data types
let userSchema: Schema = new Schema({
  userName  : String,
  password  : String,
  firstName : String,
  lastName  : String,
  email     : String,
  activated : Boolean,
  roles     : [String]
});

// interface we want to code to?
export interface IUserModel extends Model<IUserDocument> {/* any custom methods here */}

// stumped here
export class User {
  constructor() {}
}
Tim McNamara
  • 1,368
  • 2
  • 10
  • 11

24 Answers24

160

Here's how I do it:

export interface IUser extends mongoose.Document {
  name: string; 
  somethingElse?: number; 
};

export const UserSchema = new mongoose.Schema({
  name: {type:String, required: true},
  somethingElse: Number,
});

const User = mongoose.model<IUser>('User', UserSchema);
export default User;
Rsh
  • 7,214
  • 5
  • 36
  • 45
Louay Alakkad
  • 7,132
  • 2
  • 22
  • 45
  • 2
    sorry, but how is 'mongoose' defined in TS? – Tim McNamara Dec 27 '15 at 16:51
  • 13
    `import * as mongoose from 'mongoose';` or `import mongoose = require('mongoose');` – Louay Alakkad Dec 27 '15 at 16:52
  • This is solid, thanks! Can I ask how you're importing and using the model in your app? – Aarmora May 12 '16 at 20:25
  • 1
    Something like this: `import User from '~/models/user'; User.find(/*...*/).then(/*...*/);` – Louay Alakkad May 14 '16 at 17:44
  • How would one define Mongoose object id? – Oliver Dixon Jul 22 '16 at 16:46
  • @Louy when using this `import User from '~/models/user'; `, it looks like it can't determine which export to use. Has somethign changed with TypeScript? To get this to work I have to use: `import User = require('./models/user')` and reference the model with `User.User.find()...` – Douglas Gaskell Aug 11 '16 at 03:44
  • `import User ...` can be used with `export default const User`. `import {User} ...` can be used with `export const User`. – Louay Alakkad Aug 11 '16 at 16:49
  • Updated my answer. Sorry about the confusion. – Louay Alakkad Aug 11 '16 at 16:50
  • How would you add statics with this? – Nepoxx Aug 23 '16 at 20:40
  • 1
    Don't forget to install the mongoose typings, otherwise many IDEs will not flow with this. `typings install mongoose` – Augie Gardner Sep 28 '16 at 08:12
  • Would this work if the IUser interface also needed to work in the browser? It seems to me The other answer below from Mr. Imre seems like it would work better in this case, but I haven't tried your approach.. – John Lockwood Oct 01 '16 at 17:10
  • 3
    Last line (export default const User...) does not work for me. I need to split the line, as proposed in http://stackoverflow.com/questions/35821614/typescript-compile-error-error-ts1109-expression-expected – Sergio Nov 02 '16 at 10:29
  • @Louy If somethingElse property in the interface IUser was not a number but another interface say IRole, whether the IRole interface would also extend mongoose.Document? – Aniket May 25 '17 at 09:12
  • @Aniket if it was a subdocument yes it should extend `Document`, otherwise I don't think so. – Louay Alakkad May 28 '17 at 17:55
  • 13
    I can do `let newUser = new User({ iAmNotHere: true })` without any errors in the IDE or at compiling. So what is the reason for creating an interface? – Lupurus Nov 30 '17 at 21:09
  • Can you confirm that if you want an instance method to not have an error you must add the instance method and put it in the interface? – Diesel Feb 03 '19 at 21:12
  • What's the type of the instantiation of the generated "class" from `const User = mongoose.model('User', UserSchema);` ? This answer is otherwise incomplete. I.e. `const myUser: ?? = new User({...});` – José Cabo Dec 11 '19 at 18:08
  • The problem with this is that it duplicates every single field between the interface and the schema, which is a [maintenance risk](https://stackoverflow.com/questions/34482136/mongoose-the-typescript-way/61154023#61154023). – Dan Dascalescu Apr 12 '20 at 17:13
  • 1
    I see this answer over and over the whole internet, but what I dont like about it is that it is not DRY. Each time you want to add a field it must be done two times. There's a clear duplication of code with this approach. – Hernan Apr 16 '21 at 13:39
  • do I get the ide support if I add this interface? I am not getting it after adding the interface like this. – MBK Jul 14 '21 at 12:12
  • Please note that extending Document is [discouraged](https://mongoosejs.com/docs/typescript.html#using-extends-document) by mongoose devs. – MZPL Jun 24 '22 at 11:43
  • Please note that this approach is not recommended by mongoose docs and will be dropped in the next major version. https://mongoosejs.com/docs/typescript.html#using-extends-document – Safwat Fathi Feb 18 '23 at 18:19
40

Another alternative if you want to detach your type definitions and the database implementation.

import {IUser} from './user.ts';
import * as mongoose from 'mongoose';

type UserType = IUser & mongoose.Document;
const User = mongoose.model<UserType>('User', new mongoose.Schema({
    userName  : String,
    password  : String,
    /* etc */
}));

Inspiration from here: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models

Gábor Imre
  • 5,899
  • 2
  • 35
  • 48
  • 4
    Does the `mongoose.Schema` definition here duplicate the fields from `IUser`? Given that `IUser` is defined in a *different file* the [risk that the fields with get out of sync](https://stackoverflow.com/questions/34482136/mongoose-the-typescript-way/61154023#61154023) as the project grows in complexity and number of developers, is quite high. – Dan Dascalescu Apr 12 '20 at 17:15
  • Yes, this is a valid argument worth considering. Using component integration tests may help reducing the risks though. And note that there are approaches and architectures where the type declarations and the DB implementations are separated whether it's done via an ORM (as you proposed) or manually (like in this answer). No silver bullet there is... <(°.°)> – Gábor Imre Apr 14 '20 at 10:59
  • One bullet might be to [generate code](https://github.com/dotansimha/graphql-code-generator/issues/3859) from the GraphQL definition, for TypeScript and mongoose. – Dan Dascalescu Apr 14 '20 at 23:51
  • For the ones looking for automatic inference of the types: Assuming UserSchema is already declared and `InferSchemaType` and `Document` imported from Mongoose. `type UserType = InferSchemaType & Document` – Henrique Aron May 24 '23 at 20:33
37

Most answers here repeat the fields in the TypeScript class/interface, and in the mongoose schema. Not having a single source of truth represents a maintenance risk, as the project becomes more complex and more developers work on it: fields are more likely to get out of sync. This is particularly bad when the class is in a different file vs. the mongoose schema.

To keep fields in sync, it makes sense to define them once. There are a few libraries that do this:

I haven't yet been fully convinced by any of them but typegoose seems actively maintained, and the developer accepted my PRs.

To think one step ahead: when you add a GraphQL schema into the mix, another layer of model duplication appears. One way to overcome this problem might be to generate TypeScript and mongoose code from the GraphQL schema.

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
  • Using Graphql to generate typescript types will be problematic if the entity has additional graphql fields which don't exist in Mongoose schema, because the type will have not just the mongoose schema fields but also the additional fields from graphql. – Yos Feb 21 '21 at 09:07
27

Sorry for necroposting but this can be still interesting for someone. I think Typegoose provides more modern and elegant way to define models

Here is an example from the docs:

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

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

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

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

// UserModel is a regular Mongoose Model with correct types
(async () => {
    const u = new UserModel({ name: 'JohnDoe' });
    await u.save();
    const user = await UserModel.findOne();

    // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
    console.log(user);
})();

For an existing connection scenario, you can use as the following (which may be more likely in the real situations and uncovered in the docs):

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

const conn = mongoose.createConnection('mongodb://localhost:27017/test');

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

// Notice that the collection name will be 'users':
const UserModel = new User().getModelForClass(User, {existingConnection: conn});

// UserModel is a regular Mongoose Model with correct types
(async () => {
    const u = new UserModel({ name: 'JohnDoe' });
    await u.save();
    const user = await UserModel.findOne();

    // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
    console.log(user);
})();
Jeff Tian
  • 5,210
  • 3
  • 51
  • 71
Dimanoid
  • 6,999
  • 4
  • 40
  • 55
  • 8
    I also came to this conclusion, but am worried that `typegoose` doesn't have enough support... checking their npm stats, it's only 3k weekly downloads, and rn there are almost 100 open Github issues, most of which don't have comments, and some of which looks like they should have been closed a long time ago – Corbfon Jan 21 '19 at 16:33
  • @Corbfon Did you try it? If so, what were your findings? If not, was there anything else that made you decide not to use it? I generally see some people worrying about complete support, but it seems those who actually use it are quite happy with it – N4ppeL Oct 02 '19 at 10:25
  • 2
    @N4ppeL I wouldn't go with `typegoose` - we ended up manually handling our typing, similar to [this post](https://brianflove.com/2016/10/04/typescript-declaring-mongoose-schema-model/), it looks like `ts-mongoose` might have some promise (as suggested in later answer) – Corbfon Oct 04 '19 at 01:57
  • 4
    Never apologize for "necroposting". [As you now know...] There's even a badge (though [it ***is*** named Necromancer](https://stackoverflow.com/help/badges/17/necromancer) ;^D) for doing just this! Necroposting new information and ideas is encouraged! – ruffin Mar 06 '20 at 14:47
  • 2
    @ruffin: I also really don't understand the stigma against posting new and up-to-date solutions to problems. – Dan Dascalescu Apr 09 '20 at 04:31
  • Typegoose is currently being maintained and the maintainer is helpful. It also works fairly well. – m8a Aug 16 '20 at 16:30
17

Try ts-mongoose. It uses conditional types to do the mapping.

import { createSchema, Type, typedModel } from 'ts-mongoose';

const UserSchema = createSchema({
  username: Type.string(),
  email: Type.string(),
});

const User = typedModel('User', UserSchema);
sky
  • 685
  • 7
  • 8
9

Here's a strong typed way to match a plain model with a mongoose schema. The compiler will ensure the definitions passed to mongoose.Schema matches the interface. Once you have the schema, you can use

common.ts

export type IsRequired<T> =
  undefined extends T
  ? false
  : true;

export type FieldType<T> =
  T extends number ? typeof Number :
  T extends string ? typeof String :
  Object;

export type Field<T> = {
  type: FieldType<T>,
  required: IsRequired<T>,
  enum?: Array<T>
};

export type ModelDefinition<M> = {
  [P in keyof M]-?:
    M[P] extends Array<infer U> ? Array<Field<U>> :
    Field<M[P]>
};

user.ts

import * as mongoose from 'mongoose';
import { ModelDefinition } from "./common";

interface User {
  userName  : string,
  password  : string,
  firstName : string,
  lastName  : string,
  email     : string,
  activated : boolean,
  roles     : Array<string>
}

// The typings above expect the more verbose type definitions,
// but this has the benefit of being able to match required
// and optional fields with the corresponding definition.
// TBD: There may be a way to support both types.
const definition: ModelDefinition<User> = {
  userName  : { type: String, required: true },
  password  : { type: String, required: true },
  firstName : { type: String, required: true },
  lastName  : { type: String, required: true },
  email     : { type: String, required: true },
  activated : { type: Boolean, required: true },
  roles     : [ { type: String, required: true } ]
};

const schema = new mongoose.Schema(
  definition
);

Once you have your schema, you can use methods mentioned in other answers such as

const userModel = mongoose.model<User & mongoose.Document>('User', schema);
bingles
  • 11,582
  • 10
  • 82
  • 93
  • 1
    This is the only correct answer. None of the other answers actually ensured type compatability between the schema and the type/interface. – Jamie S Aug 12 '19 at 22:32
  • @JamieStrauss: what about [not duplicating the fields in the first place](https://stackoverflow.com/questions/34482136/mongoose-the-typescript-way#comment108118780_59792239)? – Dan Dascalescu Apr 09 '20 at 05:38
  • 1
    @DanDascalescu I don't think you understand how types work. – Jamie S Apr 15 '20 at 03:50
  • 1
    This answer is so underrated. Needs more upvotes. It's the best solution for ensuring type compatibility between document interface and schema as @JamieS already pointed out. For my use-case, I had to add `default?: any` to the Field type and made required optional. – Valkay Jul 25 '21 at 07:18
  • 1
    @Valkay thanks for reminding me of this gem. And you're right - this answer is criminally underrated. – Jamie S Jul 27 '21 at 09:22
6

Just add another way (@types/mongoose must be installed with npm install --save-dev @types/mongoose)

import { IUser } from './user.ts';
import * as mongoose from 'mongoose';

interface IUserModel extends IUser, mongoose.Document {}

const User = mongoose.model<IUserModel>('User', new mongoose.Schema({
    userName: String,
    password: String,
    // ...
}));

And the difference between interface and type, please read this answer

This way has a advantage, you can add Mongoose static method typings:

interface IUserModel extends IUser, mongoose.Document {
  generateJwt: () => string
}
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
Hongbo Miao
  • 45,290
  • 60
  • 174
  • 267
  • where did you define `generateJwt`? – rels Dec 15 '16 at 08:29
  • 1
    @rels `const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));` basically, `generateJwt` becomes another property of the model. – a11smiles Feb 03 '17 at 00:39
  • Would you just add it as a method in this fashion or would you connect it to the methods property? – user1790300 Jul 11 '17 at 21:25
  • 1
    This should be the accepted answer as It detaches user definition and user DAL. If you want to switch from mongo to another db provider, you won't have to change user interface. – Rafael del Rio Dec 04 '17 at 15:37
  • 1
    @RafaeldelRio: the question was about using mongoose with TypeScript. Switching to another DB is antithetic to this goal. And the problem with separating the schema definition from the `IUser` interface declaration *in a different file* is that the [risk of fields getting out of sync](https://stackoverflow.com/questions/34482136/mongoose-the-typescript-way/61154023#61154023) as the project grows in the number of complexity and developers, is quite high. – Dan Dascalescu Apr 12 '20 at 17:23
6

2023 Update

The new recommended way of typing documents is using a single interface. To type documents in your application, you should use HydratedDocument:

import { HydratedDocument, model, Schema } from "mongoose";

interface Animal {
    name: string;
}

const animalSchema = new Schema<Animal>({
    name: { type: String, required: true },
});

const AnimalModel = model<Animal>("Animal", animalSchema);

const animal: HydratedDocument<Animal> = AnimalModel.findOne( // ...

Mongoose advises against extending document.

https://mongoosejs.com/docs/typescript.html

Sir hennihau
  • 1,495
  • 4
  • 18
  • 31
5

Here's how guys at Microsoft do it. here

import mongoose from "mongoose";

export type UserDocument = mongoose.Document & {
    email: string;
    password: string;
    passwordResetToken: string;
    passwordResetExpires: Date;
...
};

const userSchema = new mongoose.Schema({
    email: { type: String, unique: true },
    password: String,
    passwordResetToken: String,
    passwordResetExpires: Date,
...
}, { timestamps: true });

export const User = mongoose.model<UserDocument>("User", userSchema);

I recommend to check this excellent starter project out when you add TypeScript to your Node project.

https://github.com/microsoft/TypeScript-Node-Starter

NFT Master
  • 1,400
  • 12
  • 13
  • 2
    That duplicates every single field between mongoose and TypeScript, which creates a maintenance risk as the model becomes more complex. Solutions like `ts-mongoose` and `typegoose` solve that problem, though admittedly with quite a bit of syntactic cruft. – Dan Dascalescu Apr 09 '20 at 05:36
5

If you want to ensure that your schema satisfies the model type and vice versa , this solution offers better typing than what @bingles suggested:

The common type file: ToSchema.ts (Don't panic! Just copy and paste it)

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

type NonOptionalKeys<T> = { [k in keyof T]-?: undefined extends T[k] ? never : k }[keyof T];
type OptionalKeys<T> = Exclude<keyof T, NonOptionalKeys<T>>;
type NoDocument<T> = Exclude<T, keyof Document>;
type ForceNotRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required?: false };
type ForceRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required: SchemaTypeOpts<any>['required'] };

export type ToSchema<T> = Record<NoDocument<NonOptionalKeys<T>>, ForceRequired | Schema | SchemaType> &
   Record<NoDocument<OptionalKeys<T>>, ForceNotRequired | Schema | SchemaType>;

and an example model:

import { Document, model, Schema } from 'mongoose';
import { ToSchema } from './ToSchema';

export interface IUser extends Document {
   name?: string;
   surname?: string;
   email: string;
   birthDate?: Date;
   lastLogin?: Date;
}

const userSchemaDefinition: ToSchema<IUser> = {
   surname: String,
   lastLogin: Date,
   role: String, // Error, 'role' does not exist
   name: { type: String, required: true, unique: true }, // Error, name is optional! remove 'required'
   email: String, // Error, property 'required' is missing
   // email: {type: String, required: true}, // correct 
   // Error, 'birthDate' is not defined
};

const userSchema = new Schema(userSchemaDefinition);

export const User = model<IUser>('User', userSchema);


  • Well, promissing, but no type-checking! If email is defined `email: string` in the interface but in the schema is defined `email: Number` it does not show any errors =( – Omar Omeiri May 05 '21 at 16:41
4

I am a fans of Plumier, it has mongoose helper, but it can be used standalone without Plumier itself. Unlike Typegoose its took different path by using Plumier's dedicated reflection library, that make it possible to use cools stuff.

Features

  1. Pure POJO (domain doesn't need to inherit from any class, nor using any special data type), Model created automatically inferred as T & Document thus its possible to access document related properties.
  2. Supported TypeScript parameter properties, it's good when you have strict:true tsconfig configuration. And with parameter properties doesn't require decorator on all properties.
  3. Supported field properties like Typegoose
  4. Configuration is the same as mongoose so you will get easily familiar with it.
  5. Supported inheritance that's make the programming more natural.
  6. Model analysis, showing model names and its appropriate collection name, configuration applied etc.

Usage

import model, {collection} from "@plumier/mongoose"


@collection({ timestamps: true, toJson: { virtuals: true } })
class Domain {
    constructor(
        public createdAt?: Date,
        public updatedAt?: Date,
        @collection.property({ default: false })
        public deleted?: boolean
    ) { }
}

@collection()
class User extends Domain {
    constructor(
        @collection.property({ unique: true })
        public email: string,
        public password: string,
        public firstName: string,
        public lastName: string,
        public dateOfBirth: string,
        public gender: string
    ) { super() }
}

// create mongoose model (can be called multiple time)
const UserModel = model(User)
const user = await UserModel.findById()
bonjorno
  • 201
  • 2
  • 12
3

Here is the example from Mongoose documentation, Creating from ES6 Classes Using loadClass(), converted to TypeScript:

import { Document, Schema, Model, model } from 'mongoose';
import * as assert from 'assert';

const schema = new Schema<IPerson>({ firstName: String, lastName: String });

export interface IPerson extends Document {
  firstName: string;
  lastName: string;
  fullName: string;
}

class PersonClass extends Model {
  firstName!: string;
  lastName!: string;

  // `fullName` becomes a virtual
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(v) {
    const firstSpace = v.indexOf(' ');
    this.firstName = v.split(' ')[0];
    this.lastName = firstSpace === -1 ? '' : v.substr(firstSpace + 1);
  }

  // `getFullName()` becomes a document method
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  // `findByFullName()` becomes a static
  static findByFullName(name: string) {
    const firstSpace = name.indexOf(' ');
    const firstName = name.split(' ')[0];
    const lastName = firstSpace === -1 ? '' : name.substr(firstSpace + 1);
    return this.findOne({ firstName, lastName });
  }
}

schema.loadClass(PersonClass);
const Person = model<IPerson>('Person', schema);

(async () => {
  let doc = await Person.create({ firstName: 'Jon', lastName: 'Snow' });
  assert.equal(doc.fullName, 'Jon Snow');
  doc.fullName = 'Jon Stark';
  assert.equal(doc.firstName, 'Jon');
  assert.equal(doc.lastName, 'Stark');

  doc = (<any>Person).findByFullName('Jon Snow');
  assert.equal(doc.fullName, 'Jon Snow');
})();

For the static findByFullName method, I couldn't figure how get the type information Person, so I had to cast <any>Person when I want to call it. If you know how to fix that please add a comment.

orad
  • 15,272
  • 23
  • 77
  • 113
  • Like [other answers](https://stackoverflow.com/questions/34482136/mongoose-the-typescript-way#comment108118805_53789111), this approach duplicates the fields between the interface and the schema. That could be avoided by having a single source of truth, e.g. by using `ts-mongoose` or `typegoose`. The situation gets further duplicated when defining the GraphQL schema. – Dan Dascalescu Apr 11 '20 at 07:05
  • Any way to define refs with this approach? – Dan Dascalescu Apr 12 '20 at 04:48
  • Should be `class PersonModel extends Model {...}` – Máxima Alekz May 18 '23 at 23:13
2

With this vscode intellisense works on both

  • User Type User.findOne
  • user instance u1._id

The Code:

// imports
import { ObjectID } from 'mongodb'
import { Document, model, Schema, SchemaDefinition } from 'mongoose'

import { authSchema, IAuthSchema } from './userAuth'

// the model

export interface IUser {
  _id: ObjectID, // !WARNING: No default value in Schema
  auth: IAuthSchema
}

// IUser will act like it is a Schema, it is more common to use this
// For example you can use this type at passport.serialize
export type IUserSchema = IUser & SchemaDefinition
// IUser will act like it is a Document
export type IUserDocument = IUser & Document

export const userSchema = new Schema<IUserSchema>({
  auth: {
    required: true,
    type: authSchema,
  }
})

export default model<IUserDocument>('user', userSchema)

tomitheninja
  • 497
  • 2
  • 13
2

For anyone looking for a solution for existing Mongoose projects:

We recently built mongoose-tsgen to address this issue (would love some feedback!). Existing solutions like typegoose required rewriting our entire schemas and introduced various incompatibilities. mongoose-tsgen is a simple CLI tool which generates an index.d.ts file containing Typescript interfaces for all your Mongoose schemas; it requires little to no configuration and integrates very smoothly with any Typescript project.

Francesco Virga
  • 196
  • 1
  • 10
2

I find the following approach the easiest and most efficient since it validates the keys in the schema with the extra interface you define, helping you keep everything in sync.

You also get the amazing typescript autocomplete suggestions when you are adding/changing schema validator properties like maxlength, lowercase, etc on the schema.

Win win!


import { Document, model, Schema, SchemaDefinitionProperty } from "mongoose";

type TDocument<Fields> = Fields & Document;
type TSchema<Fields> = Record<keyof Fields, SchemaDefinitionProperty>;

type UserFields = {
  email: string;
  firstName?: string;
  roles?: string[];
};

const userSchema: TSchema<UserFields> = {
  email: { type: Schema.Types.String, required: true, index: true },
  firstName: { type: Schema.Types.String, maxlength: 30, trim: true },
  roles: [
    { type: Schema.Types.String, maxlength: 20, lowercase: true },
  ],
};

export const User = model<TDocument<UserFields>>(
  "User",
  new Schema(userSchema, { timestamps: true })
);

Best part! you could reuse TDocument and TSchema types for all your models.

Atul
  • 2,170
  • 23
  • 24
2

Official documents discourage TS interface to extend Document.

This approach works, but we recommend your document interface not extend Document. Using extends Document makes it difficult for Mongoose to infer which properties are present on query filters, lean documents, and other cases.

TS Interface

export interface IPerson {
  firstName: string;
  lastName: string;
  fullName: string;
}

Schema

    const personSchema = new Schema<IPerson>({
      //You get intellisense of properties so less error prone
      firstName:{type:String},
      lastName:{type:String}
    })

   personSchema.virtual('fullName').get(function(this:IPerson) {
    return this.firstName + " " this.lastName
   });

   export const User = model<IPerson>('person',personSchema)
helloworld
  • 2,179
  • 3
  • 24
  • 39
2

v6.9 update

You don't need to create a type or an interface no more. You only need a schema to generate the corresponding types:

import { model, Schema, HydratedDocumentFromSchema, InferSchemaType } from "mongoose";

const UserSchema = new Schema({
  name: { type: String, required: true },
  somethingElse: Number
});

// Already typed
export const UserModel = model('User', UserSchema);

// Type of an hydrated document (with all the getters, etc...)
export type THydratedUserModel = HydratedDocumentFromSchema<typeof UserSchema>;

// Only the fields defined in the shema
export type TUserModel = InferSchemaType<typeof UserSchema>;

⚠️ As of writing this, these type helpers (HydratedDocumentFromSchema and InferSchemaType) are undocumented.

Herobrine
  • 1,661
  • 14
  • 12
  • Cool! But I wonder why `export interface TUserModel = InferSchemaType;` doesn't work as well ... – Bersan Aug 03 '23 at 20:51
1

Mongoose introduced officially supported TypeScript bindings in v5.11.0. https://mongoosejs.com/docs/typescript.html describes Mongoose's recommended approach to working with Mongoose in TypeScript.

1

As per mongoose docs

Alternatively, your document interface can extend Mongoose's Document class.

We strongly recommend against using this approach, its support will be dropped in the next major version as it causes major performance issues.

Instead you can use HydratedDocument

export interface User {
  name: string;
  email: string;
  password: string;
  phone: string;
  address: string[];
  orders: ObjectId[];
}
export type UserDoc = HydratedDocument<User>
Safwat Fathi
  • 758
  • 9
  • 16
0

Here is an example based off the README for the @types/mongoose package.

Besides the elements already included above it shows how to include regular and static methods:

import { Document, model, Model, Schema } from "mongoose";

interface IUserDocument extends Document {
  name: string;
  method1: () => string;
}
interface IUserModel extends Model<IUserDocument> {
  static1: () => string;
}

var UserSchema = new Schema<IUserDocument & IUserModel>({
  name: String
});

UserSchema.methods.method1 = function() {
  return this.name;
};
UserSchema.statics.static1 = function() {
  return "";
};

var UserModel: IUserModel = model<IUserDocument, IUserModel>(
  "User",
  UserSchema
);
UserModel.static1(); // static methods are available

var user = new UserModel({ name: "Success" });
user.method1();

In general, this README appears to be a fantastic resource for approaching types with mongoose.

webelo
  • 1,646
  • 1
  • 14
  • 32
  • This approach duplicates the definition of every field from `IUserDocument` into `UserSchema`, which creates a maintenance risk as the model becomes more complex. Packages like `ts-mongoose` and `typegoose` attempt to solve that problem, though admittedly with quite a bit of syntactic cruft. – Dan Dascalescu Apr 11 '20 at 07:01
0

The latest mongoose package has come with typescript support. You don't need to use @types/mongoose anymore. See my example here.

https://jasonching2005.medium.com/complete-guide-for-using-typescript-in-mongoose-with-lean-function-e55adf1189dc

Jason Ching
  • 1,991
  • 1
  • 19
  • 23
0

Well, I found the following link really really helpful where the author has described each and every step in details without using any library.

Typescript With MongoDB and Node/Express

This has really really helped me and hoping will be very helpful for those searching for a solution without installing any extra plugin.

However, if you like you can give a try to TypeORM and TypeGoose

But I prefer to go without installing any library :-).

Khuram
  • 1,820
  • 1
  • 26
  • 33
-1

Not sure this is what you are looking for but there's a package called Typegoose

HexaCrop
  • 3,863
  • 2
  • 23
  • 50
-5

TypeORM is a better and modern solution. It supports both JavaScript and TypeScript.

TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES5, ES6, ES7, ES8).

It has lots of features.

Its goal is to always support the latest JavaScript features and provide additional features that help you to develop any kind of application that uses databases - from small applications with a few tables to large scale enterprise applications with multiple databases.

It supports most databases like mysql, mariadb, postgres, cockroachdb, sqlite, mssql, oracle, etc. and mongodb as well.

TypeORM supports both Active Record and Data Mapper patterns, unlike all other JavaScript ORMs currently in existence, which means you can write high quality, loosely coupled, scalable, maintainable applications the most productive way.

So no need to learn different ORM or frameworks for different databases.

Dexter404
  • 25
  • 3
  • 6