Typescript and Mongoose (MongoDb) Discriminators

Many thanks to Manuel Maldonado and his excellent article https://hackernoon.com/how-to-link-mongoose-and-typescript-for-a-single-source-of-truth-94o3uqc. This post carries on from where he left off.

Discriminators – the missing step.

The current project at time of writing makes use of Mongoose’s schema inheritance mechanism. That enables you to have multiple models with overlapping schemas on top of the same underlying MongoDB collection. In other words a single collection that can have multiple models. Mostly this is a cost saving exercise. For more detail see.

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

https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-mongoose#using-mongoose-discriminators-to-store-data-in-a-single-collection

How to add types to discriminators

Begin by reading Manuel’s article first!

You will see how to set up models, schemas, statics and methods that are strongly typed and play well with your IDE IntelliSense.

You are using Typescript?

You project is using Typescript and you have installed the following package?

npm i @types/mongoose

https://www.npmjs.com/package/@types/mongoose

Discriminators and Typescript

Here is the finished Base Model:

import { model, Schema } from "mongoose";

const collection = "GeneralPurpose";

const baseOptions = {
  discriminatorKey: "__type",
  collection,
  timestamps: true,
};

export const Base = model("Base", new Schema({}, baseOptions));

Here is the Base Model being used as the inherited model:

import { Document, Model, Types, Schema } from "mongoose";
import { Base } from "./Base.model";

export const MODEL_REF = "JournalBlock";

export interface JournalBlock {
  // _id?: string; // Note, _id is inherited from Document type
  blockTitle: string;
  icon: string;
  createdAt: string;
  updatedAt: string;
  parentId: string;
}

export interface JournalBlockDocument extends JournalBlock, Document {
  minify(): unknown;
}

export interface JournalBlockModel extends Model<JournalBlockDocument> {}

export const JournalBlockSchema: Schema = new Schema({
  blockTitle: { type: String, required: [true, "A title is required"] },
  icon: { type: String, required: [true, "A URL for the icon is required"]},
  introText: {
    type: String,
    required: [true, "A introduction text required"],
    },
  },
});

// Just to prove that hooks are still functioning as expected
JournalBlockSchema.pre("save", function () {
   console.log("PRE SAVE", this);
 }).post("delete", function () {
   console.log("post delete", this);
});

// Add a method. In this case change the returned object
JournalBlockSchema.methods.minify = async function (
  
  this: JournalBlockDocument

) {
  const response: JournalBlock & { _id: string } = {
    _id: this._id,
    icon: this.icon,
    introText: this.introText,
    createdAt: this.createdAt,
    updatedAt: this.updatedAt,
    parentId: this.parentId,
  };
  return response;
};


// This is the magic where we connect Typescript to the Mongoose inherited base model (discriminator)

export const JournalBlockModel = Base.discriminator<
  JournalBlockDocument,
  JournalBlockModel
>(MODEL_REF, JournalBlockSchema);

Use

Import the model into for code as normal. Your IDE will predict the returned Documents from queries like “.findById” including any additional methods or statics