Mongoose the Typescript way…?

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() {}
}

Answers:

Answer

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;
Answer

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

Answer

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);
})();
Answer

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);
Answer

Just add another way:

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
}
Answer

If you've installed @types/mongoose

npm install --save-dev @types/mongoose

You can so do

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

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

PS: Copied @Hongbo Miao's answer

Answer

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);
Answer

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

Answer

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)

Answer

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.

Answer

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.

Answer

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()
Answer

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.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.