Skip to content

www-chique/mongoose-tsgen

 
 

Repository files navigation

mongoose-tsgen

A plug-n-play Typescript interface generator for Mongoose.

Version npm License

Motivation

Using Mongoose with Typescript requires duplicating Mongoose Schemas using Typescript interfaces (see this post by the Mongoose creator for an example). To mitigate this requirement, libraries such as typegoose & ts-mongoose have sprouted up which define a custom schema syntax that the library uses to generate both the Mongoose Schema and the Typescript interfaces. Unfortunately, this requires users to completely rewrite their Mongoose Schemas using an unfamiliar and less-supported syntax than Mongoose itself.

This library aims to remove these drawbacks by instead parsing your already-written Mongoose Schemas and generating associated Typescript interfaces. This removes the need to learn a whole new library and makes this library extremely simple to integrate into an existing Mongoose project.

Features

  • Automatically generate Typescript typings for each Mongoose document, model and subdocument
  • Works out of the box, don't need to rewrite your schemas
  • Includes a "Mongoose-less" version of each schema interface (Mongoose typings removed)

Compatibility

  • All Mongoose types and arrays
  • Virtual properties
  • Both Typescript and Javascript schema files
  • Typescript path aliases
  • Mongoose method, static & query functions
  • Typesafe document creation with Model.Create

Installation

$ npm install -D mongoose-tsgen
$ npx mtgen --help # print usage

The Gist

Once you've generated your typings file (see Usage), all you need to do is use the generated types in your schema definitions and throughout your project. Note that this practice is well documented online, I've found the following two Medium articles especially useful:

user.ts before:

import mongoose from "mongoose";

const UserSchema = new Schema(...);

export const User = mongoose.model("User", UserSchema);
export default User;

user.ts after:

import mongoose from "mongoose";
import { UserDocument, UserModel } from "../interfaces/mongoose.gen.ts";

const UserSchema = new Schema(...);

export const User: UserModel = mongoose.model<UserDocument, UserModel>("User", UserSchema);
export default User;

Then you can import the typings across your application from the Mongoose module and use them for document types:

import { UserDocument } from "./interfaces/mongoose.gen.ts";

async function getUser(uid: string): UserDocument {
  // user will be of type User
  const user = await User.findById(uid);
  return user;
}

async function editEmail(user: UserDocument, newEmail: string): UserDocument {
  user.email = newEmail;
  return await user.save();
}

Usage

mtgen [MODEL_PATH]

Generate a Typescript file containing Mongoose Schema typings.

Note that these docs refer to Typescript files only. If you haven't yet converted Mongoose schema definition files to Typescript, you can use the --js flag to still generate types.

USAGE
  $ mtgen [MODEL_PATH]

OPTIONS
  -c, --config=config    [default: ./] Path of `mtgen.config.json` or its root folder. CLI flag 
                         options will take precendence over settings in `mtgen.config.json`.

  -d, --dry-run          Print output rather than writing to file.

  -h, --help             Show CLI help

  -i, --imports=import   Custom import statements to add to the output file. Useful if you use 
                         third-party types  in your mongoose schema definitions. For multiple imports, 
                         specify this flag more than once. 

  -j, --js               Search for Javascript schema files rather than Typescript files. 
                         Passing this flag also triggers --no-func-types.

  -o, --output=output    [default: ./src/interfaces] Path of output file to write generated typings. 
                         If a folder path is passed, the generator will create a `mongoose.gen.ts` file 
                         in the specified folder.

  -p, --project=project  [default: ./] Path of `tsconfig.json` or its root folder.

  --augment              Augment generated typings into the 'mongoose' module.

  --no-format            Disable formatting generated files with prettier and fixing with eslint.

  --no-func-types        Disable using TS compiler API for method, static and query typings.

Specify the directory of your Mongoose schema definitions using MODEL_PATH. If left blank, all sub-directories will be searched for models/*.ts (ignores index.ts files). Files found are expected to export a Mongoose model.

See code: src/index.ts

Configuration File

All CLI options can be provided using a mtgen.config.json file. Use the --config option to provide the folder path containing this file ("./" will be searched if no path is provided). CLI options will take precendence over options in the mtgen.config.json file.

mtgen.config.json

{
  "imports": ["import Stripe from \"stripe\""],
  "output": "./src/custom/path/mongoose-types.ts"
}

Example

./src/models/user.ts

import mongoose from "mongoose";
import { UserDocument, UserModel, UserMethods, UserStatics, UserQueries } from "../interfaces/mongoose.gen.ts";

const { Schema } = mongoose;

const UserSchema = new Schema({
  email: {
    type: String,
    required: true
  },
  firstName: {
    type: String,
    required: true
  },
  lastName: {
    type: String,
    required: true
  },
  metadata: Schema.Types.Mixed,
  friends: [
    {
      uid: {
        type: Schema.Types.ObjectId,
        ref: "User",
        required: true
      },
      nickname: String
    }
  ],
  city: {
    coordinates: {
      type: [Number],
      index: "2dsphere"
    }
  }
});

// NOTE: `this: UserDocument` is required for virtual properties to tell TS the type of `this` value using the "fake this" feature
// you will need to add these in after your first ever run of the CLI
UserSchema.virtual("name").get(function (this: UserDocument) {
  return `${this.firstName} ${this.lastName}`;
});

// method functions, use Type Assertion (cast to UserMethods) for type safety
UserSchema.methods = {
  isMetadataString() {
    return typeof this.metadata === "string";
  }
} as UserMethods;

// static functions, use Type Assertion (cast to UserStatics) for type safety
UserSchema.statics = {
  async getFriends(friendUids: UserDocument["_id"][]) {
    return await this.aggregate([{ $match: { _id: { $in: friendUids } } }]);
  }
} as UserStatics;

// query functions, use Type Assertion (cast to UserQueries) for type safety
UserSchema.query = {
  populateFriends() {
    return this.populate("friends.uid", "firstName lastName");
  }
} as UserQueries;

export const User: UserModel = mongoose.model<UserDocument, UserModel>("User", UserSchema);
export default User;

generate typings

$ mtgen

generated typings file ./src/interfaces/mongoose.gen.ts

/* tslint:disable */
/* eslint-disable */

// ######################################## THIS FILE WAS GENERATED BY MONGOOSE-TSGEN ######################################## //

// NOTE: ANY CHANGES MADE WILL BE OVERWRITTEN ON SUBSEQUENT EXECUTIONS OF MONGOOSE-TSGEN.

import mongoose from "mongoose";

export interface UserFriend {
  uid: User["_id"] | User;
  nickname?: string;
  _id: mongoose.Types.ObjectId;
}

export interface UserQueries {
  populateFriends<Q extends mongoose.DocumentQuery<any, UserDocument, {}>>(this: Q): Q;
}

export interface UserMethods {
  isMetadataString<D extends UserDocument>(this: D): boolean;
}

export interface UserStatics {
  getFriends<M extends UserModel>(this: M, friendUids: UserDocument["_id"][]): Promise<any>;
}

export interface UserModel extends mongoose.Model<UserDocument, UserQueries>, UserStatics {}

export interface User {
  email: string;
  firstName: string;
  lastName: string;
  bestFriend?: mongoose.Types.ObjectId;
  friends: UserFriend[];
  city: {
    coordinates?: number[];
  };
  _id: mongoose.Types.ObjectId;
}

export type UserFriendDocument = mongoose.Types.Embedded & {
  uid: UserDocument["_id"] | UserDocument;
} & UserFriend;

export type UserDocument = mongoose.Document &
  UserMethods & {
    metadata?: any;
    friends: mongoose.Types.DocumentArray<UserFriendDocument>;
    city: {};
    name: string;
  } & User;

Development

  • The generating piece of src/helpers/parser.ts needs to be rewritten using ts-morph. Currently it builds the interfaces by appending generated lines of code to a string sequentially, with no knowledge of the AST. This leads to pretty confusing logic, using the TS compiler API would simplify it a ton.
  • Top-level schema fields that refer to the schema itself (e.g. a bestFriend property on a User schema refering to a User ID) should be typed as bestFriend: UserDocument["_id"] | UserDocument. Unfortunately Typescript does not support recursively accessing a property of a type, so this is currently typed like so: bestFriend: User["_id"] | UserDocument.
    • Eventually it would be nice to give the option to type User["_id"] as a string rather than an ObjectId (see #7), but this will not be possible until a better workaround is found for the issue above.
  • Cut down node_modules by using peer dependencies (i.e. mongoose) and stripping oclif.

About

A plug-n-play Typescript interface generator for Mongoose.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 96.0%
  • JavaScript 3.9%
  • Batchfile 0.1%