-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
docs: add detailed loadClass() TypeScript usage guide and new loadcla… #15731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
ce4bce2
84a65d5
2bb9e78
e1b0c45
12f3d24
6df877c
108230b
e7dd86a
80d60e6
ce7898f
01e9e92
9a6b83f
c2c737c
79d1730
486eea7
5d372f9
35c9ae8
00d4fb2
1a94f96
e8d0043
ab18984
013cbbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -80,3 +80,186 @@ const doc = new User({ name: 'test' }); | |||||||||||||||||||||||||||||||||||
| // Compiles correctly | ||||||||||||||||||||||||||||||||||||
| doc.updateName('foo'); | ||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # Using `loadClass()` with TypeScript | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](../api/schema.html#Schema.prototype.loadClass()). | ||||||||||||||||||||||||||||||||||||
| When using TypeScript, there are a few important typing details to understand. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ## Basic Usage | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| `loadClass()` copies static methods, instance methods, and ES getters/setters from the class onto the schema. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||||||||||||||
| class MyClass { | ||||||||||||||||||||||||||||||||||||
| myMethod() { | ||||||||||||||||||||||||||||||||||||
| return 42; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| static myStatic() { | ||||||||||||||||||||||||||||||||||||
| return 42; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| get myVirtual() { | ||||||||||||||||||||||||||||||||||||
| return 42; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const schema = new Schema({ property1: String }); | ||||||||||||||||||||||||||||||||||||
| schema.loadClass(MyClass); | ||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Mongoose does **not** automatically update TypeScript types for class members. | ||||||||||||||||||||||||||||||||||||
| To get full type support, you must manually merge types. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||||||||||||||
| interface MySchema { | ||||||||||||||||||||||||||||||||||||
| property1: string; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // `loadClass()` does NOT update TS types automatically. | ||||||||||||||||||||||||||||||||||||
| // So we must manually combine schema fields + class members. | ||||||||||||||||||||||||||||||||||||
| type MyCombined = MySchema & MyClass; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // The model type must include statics from the class | ||||||||||||||||||||||||||||||||||||
| type MyCombinedModel = Model<MyCombined> & typeof MyClass; | ||||||||||||||||||||||||||||||||||||
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // A document must combine Mongoose Document + class + schema | ||||||||||||||||||||||||||||||||||||
| type MyCombinedDocument = Document & MyCombined; | ||||||||||||||||||||||||||||||||||||
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Cast schema to satisfy TypeScript | ||||||||||||||||||||||||||||||||||||
| const MyModel = model<MyCombinedDocument, MyCombinedModel>( | ||||||||||||||||||||||||||||||||||||
| 'MyClass', | ||||||||||||||||||||||||||||||||||||
| schema as any | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| MyModel.myStatic(); | ||||||||||||||||||||||||||||||||||||
| const doc = new MyModel(); | ||||||||||||||||||||||||||||||||||||
| doc.myMethod(); | ||||||||||||||||||||||||||||||||||||
| doc.myVirtual; | ||||||||||||||||||||||||||||||||||||
| doc.property1; | ||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ## Typing `this` Inside Methods | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| You can annotate `this` in methods to enable full safety. | ||||||||||||||||||||||||||||||||||||
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||
| Note that this must be done for **each method individually**; it is not possible to set a `this` type for the entire class at once. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||||||||||||||
| class MyClass { | ||||||||||||||||||||||||||||||||||||
| // Instance method typed with correct `this` type | ||||||||||||||||||||||||||||||||||||
| myMethod(this: MyCombinedDocument) { | ||||||||||||||||||||||||||||||||||||
| return this.property1; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Static method typed with correct `this` type | ||||||||||||||||||||||||||||||||||||
| static myStatic(this: MyCombinedModel) { | ||||||||||||||||||||||||||||||||||||
| return 42; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ### Getters / Setters Limitation | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| TypeScript currently does **not** allow `this` parameters on getters/setters: | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||||||||||||||
| class MyClass { | ||||||||||||||||||||||||||||||||||||
| // error TS2784 | ||||||||||||||||||||||||||||||||||||
| get myVirtual(this: MyCombinedDocument) { | ||||||||||||||||||||||||||||||||||||
| return this.property1; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| This is a TypeScript limitation. | ||||||||||||||||||||||||||||||||||||
| See: [TypeScript issue #52923](https://github.com/microsoft/TypeScript/issues/52923) | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| As a workaround, you can cast `this` to the document type inside your getter: | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| ```ts | ||||||||||||||||||||||||||||||||||||
| get myVirtual() { | ||||||||||||||||||||||||||||||||||||
| // Workaround: cast 'this' to your document type | ||||||||||||||||||||||||||||||||||||
| const self = this as MyCombinedDocument; | ||||||||||||||||||||||||||||||||||||
| return `Name: ${self.property1}`; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
| ### Lean Documents and Plain Objects | |
| When you use `.lean()`, `.toObject()`, or `.toJSON()` on a Mongoose document, the returned value is a plain JavaScript object. This object does **not** have any of your class methods, statics, or virtuals attached. Attempting to call a class method or access a virtual on such an object will result in a runtime error. | |
| For example: | |
| ```ts | |
| const doc = await MyModel.findOne().exec(); | |
| const plain = doc.toObject(); | |
| // This will throw an error, because 'plain' does not have 'myMethod' | |
| plain.myMethod(); // TypeError: plain.myMethod is not a function | |
| // Similarly, virtuals are not available | |
| console.log(plain.myVirtual); // undefined |
If you need to use class methods or virtuals, always work with the hydrated Mongoose document, not the plain object returned by these methods.
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to mongoose type changes, this could be prevented, but would require a filter type to filter out methods, though this would not really work for getters & setters (at least not cleanly).
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Necro-Rohan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declaration order issue: The code example shows MyClass using MyCombinedDocument and MyCombinedModel types (lines 233, 236) before they are defined (lines 244-246). While TypeScript allows this within the same scope due to type hoisting, this ordering is confusing for documentation purposes. Consider reordering to define the interface and types first, then the class, then the schema and model creation for better clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great point regarding readability.
I explored reordering the declarations (as Copilot suggested), but it unfortunately creates a circular-dependency issue.
The combined types (e.g., MyCombined) depend on the MyClass definition, so they must be declared after the class. If moved before, TypeScript fails because MyClass is not yet defined.
Given that, I think the current order is the most correct.If helpful, I can add a clarifying comment like:
interface MySchema {
property1: string;
}
// 1. Declare the class first.
// It's OK to reference types (MyCombinedDocument) that aren't defined yet
// in 'this' annotations because of how TypeScript handles hoisting.
class MyClass {
myMethod(this: MyCombinedDocument) {
return this.property1;
}
static myStatic(this: MyCombinedModel) {
return 42;
}
}
// 2. Now define the combined types.
// This works because 'MyClass' has been declared.
type MyCombined = MySchema & MyClass;
type MyCombinedModel = Model<MyCombined> & typeof MyClass;
type MyCombinedDocument = Document & MyCombined;
// 3. Continue with the schema and model.
const schema = new Schema<MySchema>({ property1: String });
schema.loadClass(MyClass);
const MyModel = model<MyCombinedDocument, MyCombinedModel>(
'MyClass',
schema as any
);
@vkarpov15 Please let me know what you think — happy to update!
Necro-Rohan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Necro-Rohan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,177 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Schema, model, Document, Model, Types } from 'mongoose'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Schema, model, Document, Model, Types } from 'mongoose'; | |
| import { Schema, model, Document, Model, Types, HydratedDocument } from 'mongoose'; |
hasezoey marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
vkarpov15 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test uses Document type instead of HydratedDocument which is recommended in the documentation and used in other test files. Consider using HydratedDocument for consistency with the documentation examples and modern Mongoose TypeScript patterns. HydratedDocument provides better type inference for virtuals and methods.
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test file demonstrates a simpler typing pattern (Model<MyCombined> & typeof MyClass) that differs significantly from the recommended pattern in the documentation (Model<RawDocType, {}, Pick<MyClass, 'myMethod'>, Pick<MyClass, 'myVirtual'>> & Pick<typeof MyClass, 'myStatic'>).
The test should demonstrate the same pattern shown in the documentation for consistency. The documentation's pattern is more precise because it properly separates raw document types, query helpers, instance methods, and virtuals as distinct generic parameters.
| interface MySchema { | |
| property1: string; | |
| } | |
| // `loadClass()` does NOT update TS types automatically. | |
| // So we must manually combine schema fields + class members. | |
| type MyCombined = MySchema & MyClass; | |
| // The model type must include statics from the class | |
| type MyCombinedModel = Model<MyCombined> & typeof MyClass; | |
| // A document must combine Mongoose Document + class + schema | |
| type MyCombinedDocument = Document & MyCombined; | |
| // Raw document type (schema fields) | |
| interface MySchema { | |
| property1: string; | |
| } | |
| // Instance methods | |
| type MyInstanceMethods = Pick<MyClass, 'myMethod'>; | |
| // Virtuals | |
| type MyVirtuals = Pick<MyClass, 'myVirtual'>; | |
| // Statics | |
| type MyStatics = Pick<typeof MyClass, 'myStatic'>; | |
| // The model type using the recommended pattern | |
| type MyCombinedModel = Model<MySchema, {}, MyInstanceMethods, MyVirtuals> & MyStatics; | |
| // A document must combine Mongoose Document + schema + instance methods + virtuals | |
| type MyCombinedDocument = Document<MySchema> & MySchema & MyInstanceMethods & MyVirtuals; |
hasezoey marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused function basicLoadClassPattern.
| function basicLoadClassPattern() { | |
| class MyClass { | |
| myMethod() { return 42; } | |
| static myStatic() { return 42; } | |
| get myVirtual() { return 42; } | |
| } | |
| const schema = new Schema({ property1: String }); | |
| schema.loadClass(MyClass); | |
| interface MySchema { | |
| property1: string; | |
| } | |
| // `loadClass()` does NOT update TS types automatically. | |
| // So we must manually combine schema fields + class members. | |
| type MyCombined = MySchema & MyClass; | |
| // The model type must include statics from the class | |
| type MyCombinedModel = Model<MyCombined> & typeof MyClass; | |
| // A document must combine Mongoose Document + class + schema | |
| type MyCombinedDocument = Document & MyCombined; | |
| // Cast schema to satisfy TypeScript | |
| const MyModel = model<MyCombinedDocument, MyCombinedModel>('MyClass', schema as any); | |
| // Static function should work | |
| expectType<number>(MyModel.myStatic()); | |
| // Instance method should work | |
| const doc = new MyModel(); | |
| expectType<number>(doc.myMethod()); | |
| // Getter should work | |
| expectType<number>(doc.myVirtual); | |
| // Schema property should be typed | |
| expectType<string>(doc.property1); | |
| } | |
| class MyClass { | |
| myMethod() { return 42; } | |
| static myStatic() { return 42; } | |
| get myVirtual() { return 42; } | |
| } | |
| const schema = new Schema({ property1: String }); | |
| schema.loadClass(MyClass); | |
| interface MySchema { | |
| property1: string; | |
| } | |
| // `loadClass()` does NOT update TS types automatically. | |
| // So we must manually combine schema fields + class members. | |
| type MyCombined = MySchema & MyClass; | |
| // The model type must include statics from the class | |
| type MyCombinedModel = Model<MyCombined> & typeof MyClass; | |
| // A document must combine Mongoose Document + class + schema | |
| type MyCombinedDocument = Document & MyCombined; | |
| // Cast schema to satisfy TypeScript | |
| const MyModel = model<MyCombinedDocument, MyCombinedModel>('MyClass', schema as any); | |
| // Static function should work | |
| expectType<number>(MyModel.myStatic()); | |
| // Instance method should work | |
| const doc = new MyModel(); | |
| expectType<number>(doc.myMethod()); | |
| // Getter should work | |
| expectType<number>(doc.myVirtual); | |
| // Schema property should be typed | |
| expectType<string>(doc.property1); |
vkarpov15 marked this conversation as resolved.
Show resolved
Hide resolved
hasezoey marked this conversation as resolved.
Show resolved
Hide resolved
vkarpov15 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the earlier test, this uses Document instead of HydratedDocument. For consistency with the documentation and other test files, consider using HydratedDocument.
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the first test function, this test uses the simpler Model<MyCombined> & typeof MyClass pattern instead of the more precise Model generics pattern shown in the documentation. For consistency with the documentation examples, this should use the same typing approach with Pick utility types to properly separate instance methods, virtuals, and statics.
| type MyCombined = MySchema & MyClass; | |
| type MyCombinedModel = Model<MyCombined> & typeof MyClass; | |
| type MyCombinedDocument = Document & MyCombined; | |
| // Separate instance methods, statics, and virtuals using Pick | |
| type InstanceMethods = Pick<MyClass, 'myMethod'>; | |
| type Statics = Pick<typeof MyClass, 'myStatic'>; | |
| type Virtuals = Pick<MyClass, 'myVirtual'>; | |
| type MyCombinedDocument = Document & MySchema & InstanceMethods & Virtuals; | |
| type MyCombinedModel = Model<MyCombinedDocument> & Statics; |
Necro-Rohan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
vkarpov15 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test also uses the simpler typing pattern instead of the more precise pattern shown in the documentation. For consistency, all test functions should use the same Model generics pattern with Pick utility types as demonstrated in the documentation.
| type MyCombinedModel = Model<MyCombined> & typeof MyClass; | |
| type MyCombinedDocument = Document & MyCombined; | |
| type MyCombinedModel = Model<Pick<MyCombined, keyof MySchema>> & typeof MyClass; | |
| type MyCombinedDocument = Document & Pick<MyCombined, keyof MySchema>; |
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test also uses Document instead of HydratedDocument. For consistency with the documentation and other test files, consider using HydratedDocument.
vkarpov15 marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test uses Omit<Document, '_id'> instead of HydratedDocument. For consistency with the documentation and other test files, consider using HydratedDocument.
Copilot
AI
Nov 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused function getterLimitationTest.
| function getterLimitationTest() { | |
| interface MySchema { | |
| name: string; | |
| } | |
| class TestGetter { | |
| name: string; | |
| // TS errors if you try `this` in getter | |
| // @ts-expect-error TS2784: 'this' parameters are not allowed in getters | |
| get test(this: TestDoc): string { | |
| return this.name; | |
| } | |
| } | |
| interface TestDoc extends TestGetter, Omit<Document, '_id'> { | |
| _id: Types.ObjectId; | |
| } | |
| } |
Uh oh!
There was an error while loading. Please reload this page.