Mongoose has been receiving a lot of issues related to TypeScript support. We're currently working hard to ship officially supported TypeScript bindings, so users no longer have to rely on community-supported bindings from DefinitelyTyped. In this article, I'll present the patterns that are commonly used to build Mongoose apps with TypeScript.
Hello, World
First, install the necessary packages:
npm install mongoose typescript @types/mongoose
To get started with Mongoose, you should create a model. In TypeScript, a model is an interface that provides several ways to access documents. A document is a single object stored in MongoDB.
import { model, Schema, Model, Document } from 'mongoose';
interface IUser extends Document {
email: string;
firstName: string;
lastName: string;
}
const UserSchema: Schema = new Schema({
email: { type: String, required: true },
firstName: { type: String, required: true },
lastName: { type: String, required: true }
});
const User: Model<IUser> = model('User', UserSchema);
Suppose the above code is in the file test.ts
. Run ./node_modules/.bin/tsc test.ts
to compile the above file into a test.js
file that you can run with node ./test.js
.
Here's how you can create a new user using the above model.
(async function() {
await connect('mongodb://localhost:27017/test', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const user: IUser = await User.create({
email: 'bill@microsoft.com',
firstName: 'Bill',
lastName: 'Gates'
});
console.log('Done', user.email);
})();
Note that IUser
and User
are different. User
is technically not a class
according to @types/mongoose
, because TypeScript doesn't have good support for functions that return classes. Instead, User
is an
object that matches the Model<IUser>
interface. In particular, User
has a
constructor that returns an instance of IUser
, not User
.
const user: IUser = new User({
email: 'bill@microsoft.com',
firstName: 'Bill',
lastName: 'Gates'
});
await user.save();
Queries
Creating a model is somewhat tricky, but once you have a model, executing queries
with Mongoose and TypeScript is much easier. For example, here's how you can execute
findOne()
and find()
queries with static type checking:
const user: IUser = await User.findOne({ email: 'bill@microsoft.com' });
const users: Array<IUser> = await User.find({ email: 'bill@microsoft.com' });
TypeScript is smart enough to catch if you use the wrong type. For example, if
you instead do const users: IUser = await User.find(...)
, the TypeScript compiler
will fail with the below error:
$ ./node_modules/.bin/tsc ./test.ts
test.ts:31:9 - error TS2740: Type 'IUser[]' is missing the following properties from type 'IUser': email, firstName, lastName, increment, and 55 more.
31 const users: IUser = await User.find({ email: 'bill@microsoft.com' });
~~~~~
Found 1 error.
$
One issue with @types/mongoose
that Mongoose's TypeScript bindings will fix is
better support for query chaining. For example, the below chaining syntax is
perfectly valid Mongoose code, but the TypeScript compiler will fail with a
Property 'updateOne' does not exist
error.
// Equivalent to `User.updateOne({ email }, { firstName })`
const res: any = await User.
find({ email: 'bill@microsoft.com' }).
updateOne({}, { firstName: 'William' });
Middleware
Most Mongoose TypeScript tutorials don't discuss middleware, the only one I've
read is this detailed tutorial. The general idea is that Schema#pre()
and Schema#post()
take a generic parameter that determines the type of this
within the middleware function. That's how you can take into account the different types of middleware in Mongoose.
For example, for document middleware, like save()
, the generic parameter is the document type:
UserSchema.pre<IUser>('save', function() {
// `this` is an instance of `IUser`
console.log(this.email);
});
For query middleware, like findOne()
, the generic parameter is a Query<IUser>
:
UserSchema.pre<Query<IUser>>('findOne', function() {
// Prints "{ email: 'bill@microsoft.com' }"
console.log(this.getFilter());
});
However, it is up to you to make sure you pass the correct type, @types/mongoose
doesn't always ensure you have the correct generic type for the correct middleware.
For example, TypeScript lets the below code pass, even though this
is actually a query in the given middleware function.
UserSchema.pre<IUser>('findOne', function() {
console.log(this.email);
});
Moving On
Thus far, Mongoose works fairly well with TypeScript, but there's a lot of work to be done to make Mongoose's TypeScript bindings line up with how Mongoose is meant to be used. As a first step, we've been working on officially supported TypeScript bindings for Mongoose, as an alternative to @types/mongoose
. Please fill out this Google form if you're interested in helping test out our TypeScript bindings!