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
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.
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);
})();
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.
T & Document
thus its possible to access document related properties.strict:true
tsconfig configuration. And with parameter properties doesn't require decorator on all properties.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()
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
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.
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;