mongoose-tsgen
A plug-n-play Typescript generator for Mongoose.
Motivation
Using Mongoose with Typescript requires duplicating Mongoose Schemas using Typescript interfaces. To avoid duplication, libraries like typegoose define a custom schema syntax that is used to generate both the Mongoose Schemas and the Typescript interfaces. Unfortunately, this requires users to completely rewrite their Mongoose Schemas into an unfamiliar syntax and does not support the entire Mongoose feature set.
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 new syntax 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
- ⛑ Type-safe population
- ➕ Includes a "Mongoose-less" version of each schema interface (Mongoose typings removed)
Compatibility
- ✅ All Mongoose types, arrays and maps
- ✅ Virtual properties
- ✅ Mongoose method, static & query functions
- ✅ Multiple schemas per file
- ✅ Typescript path aliases
Mongoose version
Find your Mongoose version below and install the associated mongoose-tsgen version. Ensure to refer to each version's respective README for documentation (hyperlinked in table).
mongoose | mongoose-tsgen |
---|
6.1.5+ | latest |
5.11.19-6.1.14 | 8.4.7 |
5.11.0-5.11.18 | 7.1.3 |
<5.11.0 | 6.0.10 |
Note: For Mongoose v6.3.2 - v6.4.0
, see Known Issues first.
Installation
mongoose-tsgen can be installed globally or locally as a dev dependency. Refer to the table above to ensure you are using the correct version.
npm install -D mongoose-tsgen
npm install -D mongoose-tsgen@7.1.3
yarn add -D mongoose-tsgen
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.
user.ts before:
import mongoose from "mongoose";
const UserSchema = new Schema(...);
export const User = mongoose.model("User", UserSchema);
user.ts after:
import mongoose from "mongoose";
import { UserDocument, UserModel, UserSchema } from "../interfaces/mongoose.gen.ts";
const UserSchema: UserSchema = new Schema(...);
export const User: UserModel = mongoose.model<UserDocument, UserModel>("User", UserSchema);
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 {
const user = await User.findById(uid);
return user;
}
async function editEmail(user: UserDocument, newEmail: string): UserDocument {
user.email = newEmail;
return await user.save();
}
Note that this practice is well documented online, I've found the following two Medium articles especially useful:
Usage
mtgen <MODEL_PATH]>
Generate a Typescript file containing Mongoose Schema typings.
If you run into unknown type issues, check your Mongoose version. For Mongoose v5.11+, ensure you have removed the deprecated community typings @types/mongoose
.
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=imports 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.
-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.
--dates-as-strings Dates will be typed as strings. Useful for types returned to a frontend by API requests.
--debug Print debug information if anything isn't working
--no-format Disable formatting generated files with prettier.
--no-mongoose Don't generate types that reference mongoose (i.e. documents). Replace ObjectId with
string.
--no-populate-overload Disable augmenting mongoose with Query.populate overloads (the overloads narrow
the return type of populated documents queries).
Specify the directory of your Mongoose schema definitions using MODEL_PATH
. Defaults to "**/models/!(index).ts"
See code: src/index.ts
Blob patterns and package.json
Wrap blob patterns with double quotes like so: mtgen "models/**/*.ts"
.
Use escape characters for wrapping blob patterns in package.json
:
"scripts": {
"generate-mongoose-types": "mtgen \"models/**/*.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"
}
Use as a module
mongoose-tsgen
can also be imported or required and used programmatically. Below is an example:
import MongooseTsgen from "mongoose-tsgen";
async function run() {
const tsgen = new MongooseTsgen();
await tsgen.generateDefinitions({
flags: {
"dry-run": false,
"no-format": false,
"no-mongoose": false,
"no-populate-overload": false,
"dates-as-strings": false,
debug: false,
output: "./src/interfaces",
project: "./tsconfig.test.json"
},
args: {
model_path: "./src/helpers/tests/artifacts/**/*.ts"
}
});
}
run()
Query Population
Mongoose fields with a ref
property will be typed as RefDocument["_id"] | RefDocument
. As part of the generated file, mongoose will be augmented with Query.populate
overloads to narrow return types of populated queries (this can be disabled using the --no-populate-overload
flag). A helper type PopulatedDocument
and a type guard function IsPopulated
will also be generated to help with handling populated documents, see usage below:
import { IsPopulated, PopulatedDocument } from "../interfaces/mongoose.gen.ts";
function unsafeType(user: UserDocument) {
if (IsPopulated(user.bestFriend))) {
console.log(user.bestFriend._id)
}
}
function safeType(user: PopulatedDocument<UserDocument, "bestFriend">) {
console.log(user.bestFriend._id)
}
const user = await User.findById(uid).populate("bestFriend").exec()
safeType(user)
Both the mongoose populate
overload and the PopulateDocument
type handle nested and array types with ease; you rarely need to worry about enforcing types manually. In the case that the populated type cannot be determined, types will fallback to the generic RefDocument["_id"] | RefDocument
.
Example
./src/models/user.ts
import mongoose, { Schema } from "mongoose";
import { UserDocument, UserModel, UserSchema, UserObject } from "../interfaces/mongoose.gen.ts";
const UserSchema: UserSchema = new Schema({
email: {
type: String,
required: true
},
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
metadata: Schema.Types.Mixed,
bestFriend: {
type: Schema.Types.ObjectId,
ref: "User"
},
friends: [
{
uid: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
nickname: String
}
],
city: {
coordinates: {
type: [Number]
}
}
});
UserSchema.virtual("name").get(function (this: UserDocument) {
return `${this.firstName} ${this.lastName}`;
});
UserSchema.methods = {
isMetadataString() {
return this.metadata === "string";
}
};
UserSchema.statics = {
async getFriends(friendUids: UserDocument["_id"][]): Promise<UserObject[]> {
return await this.aggregate([{ $match: { _id: { $in: friendUids } } }]);
}
};
UserSchema.query = {
populateFriends() {
return this.populate("bestFriend", "firstName lastName");
}
};
export const User = mongoose.model<UserDocument, UserModel>("User", UserSchema);
generate typings
npx mtgen
generated typings file ./src/interfaces/mongoose.gen.ts
import mongoose from "mongoose";
export type UserFriend = {
uid: User["_id"] | User;
nickname?: string;
_id: mongoose.Types.ObjectId;
}
export type UserObject = User;
export type UserQueries = {
populateFriends: () => mongoose.Query<any, UserDocument, UserQueries> & UserQueries;
}
export type UserMethods = {
isMetadataString: (this: UserDocument) => boolean;
}
export type UserStatics = {
getFriends: (this: UserModel, friendUids: UserDocument["_id"][]) => Promise<UserObject[]>;
}
export type UserModel = mongoose.Model<UserDocument, UserQueries> & UserStatics
export type UserSchema = mongoose.Schema<UserDocument, UserModel, UserMethods, UserQueries>
export type User = {
email: string;
firstName: string;
lastName: string;
bestFriend?: User["_id"] | User;
friends: UserFriend[];
city: {
coordinates: number[];
};
_id: mongoose.Types.ObjectId;
}
export type UserFriendDocument = mongoose.Types.Subdocument & {
uid: UserDocument["_id"] | UserDocument;
nickname?: string;
_id: mongoose.Types.ObjectId;
};
export type UserDocument = mongoose.Document<mongoose.Types.ObjectId, UserQueries> &
UserMethods & {
email: string;
firstName: string;
lastName: string;
metadata?: any;
bestFriend?: UserDocument["_id"] | UserDocument;
friends: mongoose.Types.DocumentArray<UserFriendDocument>;
city: {
coordinates: mongoose.Types.Array<number>;
};
name: string;
_id: mongoose.Types.ObjectId;
};
Known Issues
Type instantiation is excessively deep and possibly infinite
This issue is present when using Mongoose v6.3.2 - v6.4.0 due to a conflict in types.
Workarounds:
- Fix your Mongoose version to
<6.3.2
or >6.4.0
. - Use the
--no-populate-overload
flag.
References:
Development