Stop writing your Mongoose schema twice.
Paste your schema and get the matching TypeScript interface back instantly, as a plain interface or the InferSchemaType style Mongoose recommends today. No install, no config file, nothing leaves your browser.
Want the full explanation behind why this matters, HydratedDocument, the populate() typing trap, and when to still hand-write an interface? Read Mongoose and TypeScript: How to Type Your Models Without Writing Everything Twice first.
How MongooseTS works
- 1
Paste the schema's field definitions
Paste the object literal you'd pass to new Schema({ ... }). A full new Schema(...) call with options as a second argument works too, only the first argument is read.
- 2
MongooseTS parses it locally
A small tokenizer walks the object literal, character by character, handling nested braces, arrays, quoted and unquoted keys, and dotted identifiers like Schema.Types.ObjectId, entirely in your browser.
- 3
Each field is resolved to a TypeScript type
String, Number, Boolean, Date, Buffer, and Map map to their TS equivalents. Schema.Types.ObjectId becomes Types.ObjectId, and Schema.Types.Mixed becomes unknown.
- 4
required, enum, ref, and arrays are read from the options object
A field without required: true becomes optional (marked with ?). An enum of strings becomes a union of string literals. A ref is remembered so a populate hint can be generated for it.
- 5
Nested objects without a type key become subdocuments
If a field's value is an object with no type key, it's treated as a nested subdocument and walked recursively into an inline object type, the same way Mongoose itself treats it.
- 6
Pick plain interface or InferSchemaType style
Toggle between a hand-writable interface and the modern InferSchemaType<typeof schema> pattern, with the resolved shape shown as a comment either way.
What each part of the generated type means
Every field in the output falls into one of these categories, matched from the options you actually wrote in the schema.
A field is only non-optional in the output if the schema explicitly sets required: true (or the [true, "message"] validator form). Everything else, including fields with a default, is marked with ? because Mongoose doesn't guarantee the value is present before save.
email: { type: String, required: true } → email: string;
age: Number → age?: number;A string enum array becomes a union of string literal types instead of the generic string, so assigning an unlisted value is a compile error, not just a runtime validation failure.
role: { type: String, enum: ["admin", "user"] }
→ role?: "admin" | "user";A field with a ref option types as Types.ObjectId, since that's what it actually holds before .populate() runs. A ready-to-paste populate snippet is generated separately, because TypeScript can't infer the populated shape on its own.
owner: { type: Schema.Types.ObjectId, ref: "User" }
→ owner?: Types.ObjectId; // ref: 'User'An object field with no type key is walked recursively and rendered as an inline object type, matching every rule above (required, enum, ref) at that nested level too.
address: { city: String, zip: String }
→ address?: { city?: string; zip?: string };[String], [{ type: String }], and array-of-subdocument all become Type[], reflecting the element's own type (including a nested object type for subdocument arrays).
tags: [String]
→ tags?: string[];Supported schema syntax
MongooseTS reads the plain object literal syntax almost every Mongoose schema is actually written in.
// Type shorthand
name: String
// Options object
email: { type: String, required: true, unique: true }
// Array of a type
tags: [String]
// Array of subdocuments
items: [{ sku: String, qty: Number }]
// Nested subdocument (no "type" key)
address: { city: String, zip: String }
// Dotted identifiers, both forms work
owner: { type: Schema.Types.ObjectId, ref: "User" }
owner: { type: mongoose.Schema.Types.ObjectId, ref: "User" }
// Enum
role: { type: String, enum: ["admin", "user", "guest"] }Not supported: function expressions (custom validators or computed defaults), template literals, and computed (`[expr]`) keys. A nested subdocument field literally named `type` is read as schema options instead, the same ambiguity Mongoose itself has.
When to use MongooseTS
| Situation |
|---|
| Adding TypeScript to an existing JS Mongoose project |
| Reviewing a teammate's schema in a PR |
| Learning InferSchemaType for the first time |
| Debugging a populate() typing error |
| Deciding whether to install a CLI generator for your whole project |
Frequently Asked Questions
What does MongooseTS do?
MongooseTS takes the field-definitions object you'd pass to new Schema({ ... }) and generates the matching TypeScript type for it, either as a plain interface you can copy into your codebase, or as the modern InferSchemaType<typeof yourSchema> pattern Mongoose itself recommends.
It reads required, default, enum, array fields, ref fields, and nested subdocuments, and reflects each of those correctly in the generated output, instead of just mapping String to string and calling it done.
Does MongooseTS send my schema to a server?
No. Parsing and type generation both run in JavaScript inside your browser tab. There's no backend, no API call, and nothing is logged or stored. If your schema has field or collection names you'd rather not paste into a random online tool, that's exactly the situation this is built for.
How is MongooseTS different from mongoose-tsgen or typegoose?
| mongoose-tsgen / typegoose | MongooseTS | |
|---|---|---|
| Requires npm install and a config file | Yes | No |
| Needs your project set up locally to run | Yes | No, paste and go |
| Good for generating types across a whole codebase | Yes | Not the goal, use a CLI generator for that |
| Good for checking one schema before installing anything | No | Yes, this is exactly it |
mongoose-tsgen is the better choice once you've decided to generate types for your entire models directory as part of your build. typegoose goes further and replaces plain Mongoose schema syntax with decorators entirely, which is a bigger commitment than most people want for a quick check.
MongooseTS is for the moment before that decision: you're staring at one schema, in a PR or a file you're migrating, and want to see the interface it produces right now, without touching your project setup at all.
How do I type a field after calling .populate() on it?
Paste a schema with a ref field (see the "With a ref (populate)" example button) and MongooseTS detects it automatically. Below the generated type, it prints a ready-to-paste populate snippet for each ref field, using the populate generic that tells TypeScript what the field actually resolves to after the query runs:
const doc = await GroupModel.findById(id)
.populate<{ owner: UserDocument }>("owner");
doc.owner.name; // typed correctly, not Types.ObjectIdThe full explanation of why this is necessary, TypeScript can't see that .populate() changed the shape of the result at runtime, is covered in the paired guide, the populate() typing trap.
What schema syntax does MongooseTS not support?
Function expressions inside a field (a custom validate function or a default computed by a function body) aren't parsed, since that would require running arbitrary code, which this tool deliberately never does. Template literals and computed ([expr]) keys aren't supported either.
One genuine edge case: if a nested subdocument happens to have a field literally named type, MongooseTS reads that as schema options rather than a nested field, the same ambiguity Mongoose itself has, which is exactly why Mongoose's own docs recommend type: { type: String } as the escape hatch for that case.
Does MongooseTS generate types for instance methods, statics, or virtuals?
No, and it can't, those aren't part of the field-definitions object this tool reads, they're attached separately via schema.methods, schema.statics, and schema.virtual(). The generated interface is the data shape only.
The paired guide walks through typing each of those by hand, see typing instance methods and statics and virtuals and their types.