From ce4bce2a01804b8a081b174e43f2b67e76712a6c Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Sun, 9 Nov 2025 21:17:02 +0530 Subject: [PATCH 01/22] docs: add detailed loadClass() TypeScript usage guide and new loadclass.test.ts for tsd validation --- docs/typescript/statics-and-methods.md | 183 +++++++++++++++++++++++++ test/types/loadclass.test.ts | 178 ++++++++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 test/types/loadclass.test.ts diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 477b075820f..e62295b87f6 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -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()`. +When using TypeScript, there are a few important typing details to understand. + +```ts +import { Schema, Model, model, Document } from 'mongoose'; +``` + +## 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; +} + +type MyCombined = MySchema & MyClass; + +type MyCombinedModel = Model & typeof MyClass; +type MyCombinedDocument = Document & MyCombined; + +const MyModel = model( + 'MyClass', + schema as any +); +``` + +```ts +MyModel.myStatic(); // static +const doc = new MyModel(); +doc.myMethod(); // instance +doc.myVirtual; // getter +doc.property1; // schema prop +``` + +--- + +## Typing `this` Inside Methods + +You can annotate `this` in methods to enable full safety. + +```ts +class MyClass { + myMethod(this: MyCombinedDocument) { + return this.property1; + } + + 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: [https://github.com/microsoft/TypeScript/issues/52923](https://github.com/microsoft/TypeScript/issues/52923) + +--- + +## `toObject()` / `toJSON()` Caveat + +`loadClass()` attaches methods at runtime. +However, `toObject()` and `toJSON()` return **plain JavaScript objects** without these methods. + +```ts +const doc = new MyModel({ property1: 'test' }); +const pojo = doc.toObject(); +``` + +Runtime: + +```ts +pojo.myMethod(); // runtime error +``` + +TypeScript: + +```ts +pojo.myMethod; // compiles (unsafe) +``` + +This is a known limitation: +TS cannot detect when class methods are dropped. + +--- + +## Limitations + +| Behavior | Supported | +| ---------------------------------------------- | --------- | +| Copy instance / static methods | ✅ | +| Copy getters/setters | ✅ | +| Automatic TS merging | ❌ | +| `this` typing in methods | ✅ | +| `this` typing in getters/setters | ❌ | +| Methods preserved in `toObject()` / `toJSON()` | ❌ | +| Methods preserved with `.lean()` | ❌ | + +--- + +## Recommended Pattern + +```ts +interface MySchema { + property1: string; +} + +class MyClass { + myMethod(this: MyCombinedDocument) { + return this.property1; + } + static myStatic(this: MyCombinedModel) { + return 42; + } +} + +const schema = new Schema({ property1: String }); +schema.loadClass(MyClass); + +type MyCombined = MySchema & MyClass; +type MyCombinedModel = Model & typeof MyClass; +type MyCombinedDocument = Document & MyCombined; + +const MyModel = model( + 'MyClass', + schema as any +); +``` + +--- + +## When Should I Use `loadClass()`? + +`loadClass()` is useful when organizing logic in ES6 classes. + +However: +✅ works fine +⚠ requires manual TS merging +⚠ methods lost in `toObject()` / `toJSON()` / `lean()` + +If you want better type inference, `methods` & `statics` on schema are recommended. + +--- + diff --git a/test/types/loadclass.test.ts b/test/types/loadclass.test.ts new file mode 100644 index 00000000000..f68e28c5491 --- /dev/null +++ b/test/types/loadclass.test.ts @@ -0,0 +1,178 @@ +import { Schema, model, Document, Model, Types } from 'mongoose'; +import { expectType } from 'tsd'; + + +// Basic usage of `loadClass` with TypeScript +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 & typeof MyClass; + + // A document must combine Mongoose Document + class + schema + type MyCombinedDocument = Document & MyCombined; + + // Cast schema to satisfy TypeScript + const MyModel = model('MyClass', schema as any); + + // Static function should work + expectType(MyModel.myStatic()); + + // Instance method should work + const doc = new MyModel(); + expectType(doc.myMethod()); + + // Getter should work + expectType(doc.myVirtual); + + // Schema property should be typed + expectType(doc.property1); +} + + +// Using `this` typing in class methods + +function thisParameterPattern() { + interface MySchema { + property1: string; + } + + 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; + } + + + // TypeScript does NOT allow `this` parameters in getters/setters. + // So we show an example error here. + get myVirtual() { + // @ts-expect-error: getter does not support `this` typing + return this.property1; + } + } + + const schema = new Schema({ property1: String }); + schema.loadClass(MyClass); + + type MyCombined = MySchema & MyClass; + type MyCombinedModel = Model & typeof MyClass; + type MyCombinedDocument = Document & MyCombined; + + const MyModel = model('MyClass2', schema as any); + + // Test static + expectType(MyModel.myStatic()); + + const doc = new MyModel({ property1: 'test' }); + + // Instance method returns string + expectType(doc.myMethod()); + + // Schema field is typed correctly + expectType(doc.property1); + + + // Getter works at runtime, but TypeScript can't type `this` in getters. + // So we accept `any`. + const virtual = doc.myVirtual; + expectType(virtual); +} + + +// ---------------------------------------------------------- +// Test that `toObject()` / `toJSON()` lose class behavior. +// But TypeScript does NOT warn you about this. +// +// This matches the behavior described in the issue: +// > doc.toObject().myMethod() compiles but fails at runtime +// ---------------------------------------------------------- +function toObjectToJSONTest() { + 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; + } + + type MyCombined = MySchema & MyClass; + type MyCombinedModel = Model & typeof MyClass; + type MyCombinedDocument = Document & MyCombined; + + const MyModel = model('MyClass3', schema as any); + + const doc = new MyModel({ property1: 'test' }); + + + // toObject(): + // returns plain object + // loses methods at runtime + // TypeScript still thinks methods exist + const pojo = doc.toObject(); + + // Schema property is still typed + expectType(pojo.property1); + + // TS still thinks class method exists (wrong at runtime) + expectType<() => number>(pojo.myMethod); + + // Same caveat applies to toJSON() + const json = doc.toJSON(); + + expectType<() => number>(json.myMethod); + expectType(json.property1); +} + + +// Getter limitation example +// TypeScript does not allow `this` param on getters + +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 { + _id: Types.ObjectId; + } +} + +export {}; + From 84a65d5a0affe69399ee2ec24bc360c03293f07d Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 20:21:52 +0530 Subject: [PATCH 02/22] Update docs/typescript/statics-and-methods.md Co-authored-by: hasezoey --- docs/typescript/statics-and-methods.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index e62295b87f6..a1dd019a23a 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -81,8 +81,6 @@ const doc = new User({ name: 'test' }); doc.updateName('foo'); ``` - - # Using `loadClass()` with TypeScript Mongoose supports applying ES6 classes to a schema using `schema.loadClass()`. From 2bb9e783b8bf4086898c062878d3a278bb8a510b Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 20:54:53 +0530 Subject: [PATCH 03/22] Update link text for typescript issue #52923 Co-authored-by: hasezoey --- docs/typescript/statics-and-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index a1dd019a23a..419465ba3e1 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -172,7 +172,7 @@ class MyClass { ``` This is a TypeScript limitation. -See: [https://github.com/microsoft/TypeScript/issues/52923](https://github.com/microsoft/TypeScript/issues/52923) +See: [TypeScript issue #52923](https://github.com/microsoft/TypeScript/issues/52923) --- From e1b0c45bd6109536cf194e2617fd55756ac7f65d Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 20:56:21 +0530 Subject: [PATCH 04/22] 1st changes according to suggestion --- docs/typescript/statics-and-methods.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 419465ba3e1..6922da340a8 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -83,12 +83,12 @@ doc.updateName('foo'); # Using `loadClass()` with TypeScript -Mongoose supports applying ES6 classes to a schema using `schema.loadClass()`. +Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.loadClass()). When using TypeScript, there are a few important typing details to understand. -```ts + ## Basic Usage @@ -130,14 +130,12 @@ const MyModel = model( 'MyClass', schema as any ); -``` -```ts -MyModel.myStatic(); // static +MyModel.myStatic(); const doc = new MyModel(); -doc.myMethod(); // instance -doc.myVirtual; // getter -doc.property1; // schema prop +doc.myMethod(); +doc.myVirtual; +doc.property1; ``` --- @@ -145,6 +143,7 @@ doc.property1; // schema prop ## Typing `this` Inside Methods You can annotate `this` in methods to enable full safety. +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 { From 12f3d24fc1a62b0dfe756fd532596a20076cb50a Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 21:32:42 +0530 Subject: [PATCH 05/22] 2st changes in loadclass doc according to suggestion --- docs/typescript/statics-and-methods.md | 25 ++++++++++++++++++++----- test/types/loadclass.test.ts | 4 +--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 6922da340a8..8572beb98e8 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -86,9 +86,6 @@ doc.updateName('foo'); Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.loadClass()). When using TypeScript, there are a few important typing details to understand. - ## Basic Usage @@ -121,11 +118,17 @@ 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 & typeof MyClass; + +// A document must combine Mongoose Document + class + schema type MyCombinedDocument = Document & MyCombined; +// Cast schema to satisfy TypeScript const MyModel = model( 'MyClass', schema as any @@ -147,10 +150,12 @@ Note that this must be done for **each method individually**; it is not possible ```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; } @@ -173,6 +178,16 @@ class MyClass { 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}`; +} +``` + --- ## `toObject()` / `toJSON()` Caveat @@ -216,7 +231,7 @@ TS cannot detect when class methods are dropped. --- -## Recommended Pattern +## Full example Code ```ts interface MySchema { @@ -256,7 +271,7 @@ However: ⚠ requires manual TS merging ⚠ methods lost in `toObject()` / `toJSON()` / `lean()` -If you want better type inference, `methods` & `statics` on schema are recommended. +If you want better type inference, [`methods`](https://mongoosejs.com/docs/guide.html#methods) & [`statics`](https://mongoosejs.com/docs/guide.html#statics) on schema are recommended. --- diff --git a/test/types/loadclass.test.ts b/test/types/loadclass.test.ts index f68e28c5491..d4653b2e645 100644 --- a/test/types/loadclass.test.ts +++ b/test/types/loadclass.test.ts @@ -105,7 +105,7 @@ function thisParameterPattern() { // Test that `toObject()` / `toJSON()` lose class behavior. // But TypeScript does NOT warn you about this. // -// This matches the behavior described in the issue: +// This matches the behavior described in the #12813: // > doc.toObject().myMethod() compiles but fails at runtime // ---------------------------------------------------------- function toObjectToJSONTest() { @@ -174,5 +174,3 @@ function getterLimitationTest() { } } -export {}; - From 6df877cb6c2d602a6e668510675901e6ed8bcd22 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 21:43:53 +0530 Subject: [PATCH 06/22] fixed markdownlint errors --- docs/typescript/statics-and-methods.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 8572beb98e8..1f3734736a2 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -86,7 +86,6 @@ doc.updateName('foo'); Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](https://mongoosejs.com/docs/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. @@ -273,5 +272,4 @@ However: If you want better type inference, [`methods`](https://mongoosejs.com/docs/guide.html#methods) & [`statics`](https://mongoosejs.com/docs/guide.html#statics) on schema are recommended. ---- - +--- \ No newline at end of file From 108230b4686edcfc94b1ab5ea809aba7e9121009 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 23:18:07 +0530 Subject: [PATCH 07/22] fixed markdownlint rule MD047 error --- docs/typescript/statics-and-methods.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 1f3734736a2..f640391c4b7 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -121,7 +121,7 @@ interface MySchema { // So we must manually combine schema fields + class members. type MyCombined = MySchema & MyClass; - // The model type must include statics from the class +// The model type must include statics from the class type MyCombinedModel = Model & typeof MyClass; // A document must combine Mongoose Document + class + schema @@ -272,4 +272,4 @@ However: If you want better type inference, [`methods`](https://mongoosejs.com/docs/guide.html#methods) & [`statics`](https://mongoosejs.com/docs/guide.html#statics) on schema are recommended. ---- \ No newline at end of file +--- From e7dd86a8eced6ff279a41c253499238dffe2c9f7 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 23:48:53 +0530 Subject: [PATCH 08/22] removed horizontal rule's --- docs/typescript/statics-and-methods.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index f640391c4b7..71522748abb 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -140,8 +140,6 @@ doc.myVirtual; doc.property1; ``` ---- - ## Typing `this` Inside Methods You can annotate `this` in methods to enable full safety. From 80d60e6cf002cd661cb581b33bddcc4e67824b3c Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 23:50:53 +0530 Subject: [PATCH 09/22] Update api link path Co-authored-by: hasezoey --- docs/typescript/statics-and-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 71522748abb..17999a4c062 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -83,7 +83,7 @@ doc.updateName('foo'); # Using `loadClass()` with TypeScript -Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.loadClass()). +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 From ce7898f293a0b086d0f5ca292f0af041e5ffe6be Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 23:51:24 +0530 Subject: [PATCH 10/22] Updating api link path for methods and statics Co-authored-by: hasezoey --- docs/typescript/statics-and-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 17999a4c062..60ea336700c 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -268,6 +268,6 @@ However: ⚠ requires manual TS merging ⚠ methods lost in `toObject()` / `toJSON()` / `lean()` -If you want better type inference, [`methods`](https://mongoosejs.com/docs/guide.html#methods) & [`statics`](https://mongoosejs.com/docs/guide.html#statics) on schema are recommended. +If you want better type inference, [`methods`](../guide.html#methods) & [`statics`](../guide.html#statics) on schema are recommended. --- From 01e9e9255e9f094c96ea9e08736244363901808e Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Mon, 10 Nov 2025 23:58:58 +0530 Subject: [PATCH 11/22] removed all Horizontal Rule's (
) below the header for better ui and clearity --- docs/typescript/statics-and-methods.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 60ea336700c..333baa009d7 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -185,8 +185,6 @@ get myVirtual() { } ``` ---- - ## `toObject()` / `toJSON()` Caveat `loadClass()` attaches methods at runtime. @@ -212,8 +210,6 @@ pojo.myMethod; // compiles (unsafe) This is a known limitation: TS cannot detect when class methods are dropped. ---- - ## Limitations | Behavior | Supported | @@ -226,8 +222,6 @@ TS cannot detect when class methods are dropped. | Methods preserved in `toObject()` / `toJSON()` | ❌ | | Methods preserved with `.lean()` | ❌ | ---- - ## Full example Code ```ts @@ -257,8 +251,6 @@ const MyModel = model( ); ``` ---- - ## When Should I Use `loadClass()`? `loadClass()` is useful when organizing logic in ES6 classes. From 9a6b83f9991dec75cfb4a99ad19dafc46520cf0d Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 00:15:38 +0530 Subject: [PATCH 12/22] adding expectError as suggested Co-authored-by: hasezoey --- test/types/loadclass.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/types/loadclass.test.ts b/test/types/loadclass.test.ts index d4653b2e645..bb47830f677 100644 --- a/test/types/loadclass.test.ts +++ b/test/types/loadclass.test.ts @@ -68,6 +68,7 @@ function thisParameterPattern() { // TypeScript does NOT allow `this` parameters in getters/setters. // So we show an example error here. get myVirtual() { + expectError(this.property1); // @ts-expect-error: getter does not support `this` typing return this.property1; } From c2c737ca188739e161ec8eaae48f27a61dc7cb3f Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 00:24:31 +0530 Subject: [PATCH 13/22] fixed the error of importing expectError --- test/types/loadclass.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/loadclass.test.ts b/test/types/loadclass.test.ts index bb47830f677..aaea6a3fc0c 100644 --- a/test/types/loadclass.test.ts +++ b/test/types/loadclass.test.ts @@ -1,5 +1,5 @@ import { Schema, model, Document, Model, Types } from 'mongoose'; -import { expectType } from 'tsd'; +import { expectType, expectError } from 'tsd'; // Basic usage of `loadClass` with TypeScript From 79d173050ba1bda2f297837bdb4a7fcb2dcac328 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 09:01:26 +0530 Subject: [PATCH 14/22] fixed inconsistent capitalization issue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/typescript/statics-and-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 333baa009d7..03bb1dcb66f 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -222,7 +222,7 @@ TS cannot detect when class methods are dropped. | Methods preserved in `toObject()` / `toJSON()` | ❌ | | Methods preserved with `.lean()` | ❌ | -## Full example Code +## Full Example Code ```ts interface MySchema { From 486eea70656778cc1de359a27029852338a749cc Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 09:04:47 +0530 Subject: [PATCH 15/22] added bullet points to section "why should i use loadclass()" Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/typescript/statics-and-methods.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 03bb1dcb66f..c6e97c55ac7 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -256,9 +256,9 @@ const MyModel = model( `loadClass()` is useful when organizing logic in ES6 classes. However: -✅ works fine -⚠ requires manual TS merging -⚠ methods lost in `toObject()` / `toJSON()` / `lean()` +- ✅ works fine +- ⚠ requires manual TS merging +- ⚠ methods lost in `toObject()` / `toJSON()` / `lean()` If you want better type inference, [`methods`](../guide.html#methods) & [`statics`](../guide.html#statics) on schema are recommended. From 5d372f90e93520d8cfbe951719dd3cfef65cb686 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 09:05:28 +0530 Subject: [PATCH 16/22] fixed grammetical error in the "issue" Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/types/loadclass.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/loadclass.test.ts b/test/types/loadclass.test.ts index aaea6a3fc0c..90af03764d1 100644 --- a/test/types/loadclass.test.ts +++ b/test/types/loadclass.test.ts @@ -106,7 +106,7 @@ function thisParameterPattern() { // Test that `toObject()` / `toJSON()` lose class behavior. // But TypeScript does NOT warn you about this. // -// This matches the behavior described in the #12813: +// This matches the behavior described in issue #12813: // > doc.toObject().myMethod() compiles but fails at runtime // ---------------------------------------------------------- function toObjectToJSONTest() { From 35c9ae82d879a7a8dc15c7d546ddffb170559cb8 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 21:01:11 +0530 Subject: [PATCH 17/22] Refactor loadClass types using HydratedDocument and Renames MySchema to RawDocType --- docs/typescript/statics-and-methods.md | 129 ++++++++++++++----------- 1 file changed, 73 insertions(+), 56 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index c6e97c55ac7..669b670f5d7 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -109,40 +109,47 @@ 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. +Mongoose does not automatically update TypeScript types for class members. To get full type support, you must manually define types using Mongoose's [Model](../api/model.html) and [HydratedDocument](../typescript.html) generics. ```ts -interface MySchema { +// 1. Define an interface for the raw document data +interface RawDocType { 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 & typeof MyClass; - -// A document must combine Mongoose Document + class + schema -type MyCombinedDocument = Document & MyCombined; - -// Cast schema to satisfy TypeScript -const MyModel = model( +// 2. Define the Model type +// This includes the raw data, query helpers, instance methods, virtuals, and statics. +type MyCombinedModel = Model< + RawDocType, + {}, + Pick, + Pick +> & Pick; + +// 3. Define the Document type +type MyCombinedDocument = HydratedDocument< + RawDocType, + Pick, + {}, + Pick +>; + +// 4. Create the Mongoose model +const MyModel = model( 'MyClass', - schema as any + schema ); -MyModel.myStatic(); +MyModel.myStatic(); const doc = new MyModel(); -doc.myMethod(); -doc.myVirtual; -doc.property1; +doc.myMethod(); +doc.myVirtual; +doc.property1; ``` ## Typing `this` Inside Methods -You can annotate `this` in methods to enable full safety. +You can annotate `this` in methods to enable full safety, using the [Model](../api/model.html) and [HydratedDocument](../typescript.html) types you defined. 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 @@ -165,15 +172,14 @@ TypeScript currently does **not** allow `this` parameters on getters/setters: ```ts class MyClass { - // error TS2784 + // error TS2784: 'this' parameters are not allowed in getters get myVirtual(this: MyCombinedDocument) { return this.property1; } } ``` -This is a TypeScript limitation. -See: [TypeScript issue #52923](https://github.com/microsoft/TypeScript/issues/52923) +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: @@ -185,31 +191,20 @@ get myVirtual() { } ``` -## `toObject()` / `toJSON()` Caveat +## `toObject()` / `toJSON()` -`loadClass()` attaches methods at runtime. -However, `toObject()` and `toJSON()` return **plain JavaScript objects** without these methods. +When using the correct [Model](../api/model.html) and [HydratedDocument](../typescript.html) generics (as shown above), Mongoose's types for `toObject()` and `toJSON()` work as expected. They will correctly return a type that includes **only the raw data** (e.g., `RawDocType`), not the methods or virtuals. + +This is a feature, as it accurately reflects the plain object returned at runtime and prevents you from trying to access methods that don't exist. ```ts const doc = new MyModel({ property1: 'test' }); const pojo = doc.toObject(); -``` - -Runtime: -```ts -pojo.myMethod(); // runtime error -``` - -TypeScript: - -```ts -pojo.myMethod; // compiles (unsafe) +// This now correctly causes a TypeScript error! +// pojo.myMethod(); // Property 'myMethod' does not exist on type 'RawDocType'. ``` -This is a known limitation: -TS cannot detect when class methods are dropped. - ## Limitations | Behavior | Supported | @@ -219,13 +214,15 @@ TS cannot detect when class methods are dropped. | Automatic TS merging | ❌ | | `this` typing in methods | ✅ | | `this` typing in getters/setters | ❌ | -| Methods preserved in `toObject()` / `toJSON()` | ❌ | +| Methods preserved in `toObject()` / `toJSON()` | ✅ | | Methods preserved with `.lean()` | ❌ | ## Full Example Code ```ts -interface MySchema { +import { Model, Schema, model, HydratedDocument } from 'mongoose'; + +interface RawDocType { property1: string; } @@ -233,22 +230,43 @@ class MyClass { myMethod(this: MyCombinedDocument) { return this.property1; } - static myStatic(this: MyCombinedModel) { + + static myStatic() { return 42; } + + get myVirtual() { + const self = this as MyCombinedDocument; + return `Hello ${self.property1}`; + } } -const schema = new Schema({ property1: String }); +const schema = new Schema({ property1: String }); schema.loadClass(MyClass); -type MyCombined = MySchema & MyClass; -type MyCombinedModel = Model & typeof MyClass; -type MyCombinedDocument = Document & MyCombined; - -const MyModel = model( +type MyCombinedModel = Model< + RawDocType, + {}, + Pick, + Pick +> & Pick; + +type MyCombinedDocument = HydratedDocument< + RawDocType, + Pick, + {}, + Pick +>; + +const MyModel = model( 'MyClass', - schema as any + schema ); + +const doc = new MyModel({ property1: 'world' }); +doc.myMethod(); +MyModel.myStatic(); +console.log(doc.myVirtual); ``` ## When Should I Use `loadClass()`? @@ -256,10 +274,9 @@ const MyModel = model( `loadClass()` is useful when organizing logic in ES6 classes. However: -- ✅ works fine -- ⚠ requires manual TS merging -- ⚠ methods lost in `toObject()` / `toJSON()` / `lean()` -If you want better type inference, [`methods`](../guide.html#methods) & [`statics`](../guide.html#statics) on schema are recommended. +* ✅ works fine +* ⚠ requires manual TS merging +* ⚠ methods lost in `toObject()` / `toJSON()` / `lean()` ---- +If you want better type inference, [`methods`](../guide.html#methods) & [`statics`](../guide.html#statics) on schema are recommended. From 00d4fb2080762c2febf395473b9d45cd181d275b Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 21:12:06 +0530 Subject: [PATCH 18/22] correcting the limitation of Methods preserved in toObject() / toJSON() --- docs/typescript/statics-and-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 669b670f5d7..1eb9d7d73dc 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -214,7 +214,7 @@ const pojo = doc.toObject(); | Automatic TS merging | ❌ | | `this` typing in methods | ✅ | | `this` typing in getters/setters | ❌ | -| Methods preserved in `toObject()` / `toJSON()` | ✅ | +| Methods preserved in `toObject()` / `toJSON()` | ❌ | | Methods preserved with `.lean()` | ❌ | ## Full Example Code From 1a94f9645e77836d385b6a54a9fc357494989ce9 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 11 Nov 2025 23:37:55 +0530 Subject: [PATCH 19/22] Fixed static this type and removed inaccurate bullet --- docs/typescript/statics-and-methods.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 1eb9d7d73dc..65ca0c00543 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -195,7 +195,7 @@ get myVirtual() { When using the correct [Model](../api/model.html) and [HydratedDocument](../typescript.html) generics (as shown above), Mongoose's types for `toObject()` and `toJSON()` work as expected. They will correctly return a type that includes **only the raw data** (e.g., `RawDocType`), not the methods or virtuals. -This is a feature, as it accurately reflects the plain object returned at runtime and prevents you from trying to access methods that don't exist. +This accurately reflects the plain object returned at runtime and prevents you from trying to access methods that don't exist. ```ts const doc = new MyModel({ property1: 'test' }); @@ -231,7 +231,7 @@ class MyClass { return this.property1; } - static myStatic() { + static myStatic(this: MyCombinedModel) { return 42; } @@ -276,7 +276,6 @@ console.log(doc.myVirtual); However: * ✅ works fine -* ⚠ requires manual TS merging -* ⚠ methods lost in `toObject()` / `toJSON()` / `lean()` +* ⚠ requires manual Typescript Types -If you want better type inference, [`methods`](../guide.html#methods) & [`statics`](../guide.html#statics) on schema are recommended. +If you want better type inference, using schema options [`methods`](../guide.html#methods) and [`statics`](../guide.html#statics) instead of `loadClass` are recommended. From e8d004374579f9b7108a29e92a50ae2d95323af0 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Sat, 15 Nov 2025 13:38:12 +0530 Subject: [PATCH 20/22] Refine loadClass() TS guide based on feedback by Removing redundant toObject()/toJSON() section and cleans up the limitations table. --- docs/typescript/statics-and-methods.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index 65ca0c00543..efa1b00a53f 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -191,21 +191,7 @@ get myVirtual() { } ``` -## `toObject()` / `toJSON()` - -When using the correct [Model](../api/model.html) and [HydratedDocument](../typescript.html) generics (as shown above), Mongoose's types for `toObject()` and `toJSON()` work as expected. They will correctly return a type that includes **only the raw data** (e.g., `RawDocType`), not the methods or virtuals. - -This accurately reflects the plain object returned at runtime and prevents you from trying to access methods that don't exist. - -```ts -const doc = new MyModel({ property1: 'test' }); -const pojo = doc.toObject(); - -// This now correctly causes a TypeScript error! -// pojo.myMethod(); // Property 'myMethod' does not exist on type 'RawDocType'. -``` - -## Limitations +## Feature Support Summary | Behavior | Supported | | ---------------------------------------------- | --------- | @@ -214,8 +200,6 @@ const pojo = doc.toObject(); | Automatic TS merging | ❌ | | `this` typing in methods | ✅ | | `this` typing in getters/setters | ❌ | -| Methods preserved in `toObject()` / `toJSON()` | ❌ | -| Methods preserved with `.lean()` | ❌ | ## Full Example Code From ab189840c8cc7eb364f79dd690acc5f120be7208 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Sat, 15 Nov 2025 13:41:16 +0530 Subject: [PATCH 21/22] fixed heading 'Getter / Setter Limitation' to maintain consistancy --- docs/typescript/statics-and-methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index efa1b00a53f..aa94a6d8b1d 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -166,7 +166,7 @@ class MyClass { } ``` -### Getters / Setters Limitation +## Getters / Setters Limitation TypeScript currently does **not** allow `this` parameters on getters/setters: From 013cbbba3208f0f16ad292385f8293560d5928c5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 17 Nov 2025 13:44:36 -0500 Subject: [PATCH 22/22] Update TypeScript documentation for loadClass() usage --- docs/typescript/statics-and-methods.md | 35 ++++++++------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md index aa94a6d8b1d..4a8ae02d40e 100644 --- a/docs/typescript/statics-and-methods.md +++ b/docs/typescript/statics-and-methods.md @@ -81,12 +81,12 @@ const doc = new User({ name: 'test' }); doc.updateName('foo'); ``` -# Using `loadClass()` with TypeScript +## Using `loadClass()` with TypeScript -Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](../api/schema.html#Schema.prototype.loadClass()). +Mongoose supports applying ES6 classes to a schema using [`schema.loadClass()`](../api/schema.html#Schema.prototype.loadClass()) as an alternative to defining statics and methods in your schema. When using TypeScript, there are a few important typing details to understand. -## Basic Usage +### Basic Usage `loadClass()` copies static methods, instance methods, and ES getters/setters from the class onto the schema. @@ -147,7 +147,7 @@ doc.myVirtual; doc.property1; ``` -## Typing `this` Inside Methods +### Typing `this` Inside Methods You can annotate `this` in methods to enable full safety, using the [Model](../api/model.html) and [HydratedDocument](../typescript.html) types you defined. 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. @@ -166,7 +166,7 @@ class MyClass { } ``` -## Getters / Setters Limitation +### Getters / Setters Limitation TypeScript currently does **not** allow `this` parameters on getters/setters: @@ -191,17 +191,7 @@ get myVirtual() { } ``` -## Feature Support Summary - -| Behavior | Supported | -| ---------------------------------------------- | --------- | -| Copy instance / static methods | ✅ | -| Copy getters/setters | ✅ | -| Automatic TS merging | ❌ | -| `this` typing in methods | ✅ | -| `this` typing in getters/setters | ❌ | - -## Full Example Code +### Full Example Code ```ts import { Model, Schema, model, HydratedDocument } from 'mongoose'; @@ -253,13 +243,10 @@ MyModel.myStatic(); console.log(doc.myVirtual); ``` -## When Should I Use `loadClass()`? - -`loadClass()` is useful when organizing logic in ES6 classes. - -However: +### When Should I Use `loadClass()`? -* ✅ works fine -* ⚠ requires manual Typescript Types +`loadClass()` is useful for defining methods and statics in classes. +If you have a strong preference for classes, you can use `loadClass()`; however, we recommend defining `statics` and `methods` in schema options as described in the first section. -If you want better type inference, using schema options [`methods`](../guide.html#methods) and [`statics`](../guide.html#statics) instead of `loadClass` are recommended. +The major downside of `loadClass()` in TypeScript is that it requires manual TypeScript types. +If you want better type inference, you can use schema options [`methods`](../guide.html#methods) and [`statics`](../guide.html#statics).