Mongoose, Typescript and Async/Await

Jay Kariesch
3 min readNov 13, 2019

Maybe you’ve arrived here while curiously poking around at Mongoose with async/await, or perhaps Typescript is just choking on your queries.

In any case, I’ll walk through writing schema, a model, creating a document, and returning a document all-the-while keeping Typescript happy.

In this article, I’ll demonstrate:

  • Creating types for models
  • Using Mongoose and Typescript with async/await
  • How to use Typescipt with Mongoose’s lean method

Typescript with Mongoose Models

First, let’s create our types. In this case I’ll create a user interface and generate a corresponding Mongoose document type:

import mongoose, { Document } from 'mongoose'interface ITodo {
title: string
message: string
}
interface ITodoDoc extends Document, ITodo {}

ITodoDoc extends Mongoose’s Document type — which is the standard return value for a query. It also extends ITodo. This combines our type with the Mongoose Document type, which allows us to tell Typescipt what to expect so it can infer this output in the future.

Next, we’ll go ahead and create out schema and model, passing in ITodoDoc as a type argument to model:

const todoSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
message: {
type: String,
required: true
}
})
const TodoModel = mongoose.model<ITodoDoc>('todo', todoSchema)

Adding the ITodoDoc argument to the model is what allows the inference to occur.

Typescript, async/await, and Mongoose Create

Let’s create our first Todo:

interface createTodoArgs {
title: string
message: string
}
const createTodo = async ({title, message}: createTodoArgs): ITodoDoc => {
let todo: TodoDoc
try {
todo = await TodoModel.create({ title, message })
} catch(ex) {
throw new Error(ex)
}
return todo
}

Since we’re using create and async/await, our data is returned in a very straight forward manner without any callback boilerplate.

Typescript, async/await, and Mongoose Queries

Now let’s write a basic query:

const getTodo = async (id: string): ITodoDoc => {
let todo: TodoDoc
try {
todo = await TodoModel.findById(id)
} catch(ex) {
throw new Error(ex)
}
return todo
}

Again, pretty simple. Let’s take a look at a variation without try/catch to examine what happens with and without a typed model:

...const TodoModel = mongoose.model('todo', todoSchema)const getTodo = async (id: string): ITodoDoc => {    // todo is equal to ITodoDoc
const todo = await TodoModel.findById(id)
// typescript won't recognize title as a property of todo
if(todo.title) {
...
}

return todo
}

Without a typed model, Typescript will choke at the if statement, as it would not know todo has a title property, and would only identify the basic Mongoose Document type properties. However, with the following line from a previous example above…

const TodoModel = mongoose.model<ITodoDoc>('todo', todoSchema)

…it would recognize that todo has the property title, and eliminate the error feedback.

Let’s look at a trickier example: lean

Typescript and Mongoose’s Lean Method

What is lean? From Mongoose:

The lean option tells Mongoose to skip hydrating the result documents. This makes queries faster and less memory intensive, but the result documents are plain old JavaScript objects (POJOs), not Mongoose documents.

This means that, instead of your todo query returning ITodoDoc, it would simply return ITodo, so you’ll no longer have to destructure the properties you do need from the returned Document. The gotcha here is that, when using lean, you must explicitly tell Typescript that the return type, ITodo, will differ from the type assigned to our TodoModel,ITodoDoc. If we don’t, Typescript will choke, and it might leave you scratching your head.

Here’s an example of properly using Typescript with lean:

const getTodo = async (id: string): ITodo => {    // todo is equal to ITodo
const todo = await TodoModel.findById(id).lean<ITodo>()
if(todo.title) {
...
}

return todo
}

This works because lean accepts a generic, and passes it into the Query type:

lean<T>(bool?: boolean | object): Query<T> & QueryHelpers;

That’s all, folks!

Although this is a very rudimentary glimpse, I hope it helps you along your development journey.

--

--