Repository: Automattic/mongoose (2025-06-21T17:49:53Z)

Issue:
Can't get typing for output of toJSON
### Prerequisites

- [x] I have written a descriptive issue title

### Mongoose version

8.15.1

### Node.js version

22.13.1

### MongoDB version

8.0.4

### Operating system

Linux

### Operating system version (i.e. 20.04, 11.3, 10)

_No response_

### Issue

The `toJSON` method returns `any` for the output type. I've attempted to specify the output type of the transform function, but due to the function signatures defined in `document.d.ts`, it gets widened to `any` making it pointless. Is there some method I'm missing on how to do this? Everything I've been able to find online about this (and what I've seen after poking around mongoose internals) seems to indicate there's no way to do this currently.

If it isn't possible, would it make sense to update the toJSON return type to be dynamic? It might work if the transform function required a typing to be supplied (using `any` as a fallback to avoid breaking backwards compat) with generics and then toJSON would just use whatever the return type of the transform function is.


Discussion:
I'm unable to repro, the following TypeScript compiles and shows that `toJSON()` returns the correct type. Can you modify the following script to demonstrate the issue you're seeing?

```ts

import mongoose from 'mongoose';

const TestModel = mongoose.model(
  'Test',
  new mongoose.Schema({ name: String })
);

const doc = new TestModel();
getName(doc.toJSON());

function getName(obj: { name?: string | null }) {
  return obj.name || 'Test';
}

```
I should've clarified earlier. The issue is specifically when changing the json output within the transform function. So if you do this:

```javascript
import mongoose from 'mongoose'

const TestSchema  = new mongoose.Schema({
  name: String,
  testField: {
    type: String,
    required: true,
    default: 'blah'
})

TestSchema.set('toJSON', {
  transform(doc, ret) {
    delete ret.testField
  },
})

const TestModel = mongoose.model('Test', TestSchema)

const doc = new TestModel()

/**
If things were working correctly, this would throw a typescript error since our json output should not have the testField anymore.
With the way `toJSON` is currently written, there's no way to indicate to typescript what changes have occurred within `toJSON`.
Any return types you set on the transform method are ignored due to type widening afaik.
*/
getTestField(doc.toJSON())

function getTestField(obj: { testField: string }) {
  return obj.testField
}
```
@vkarpov15 have you been able to reproduce it with my clarifications?
Yes, I've been able to repro, we're trying to figure out a more reasonable workaround.

Right now, all you can do is pass in a generic param to `toJSON()` to override the return type as follows

```ts
TestSchema.set('toJSON', {
  transform(doc, ret) {
    delete ret.testField;
    return ret;
  },
})

getTestField(doc.toJSON<Omit<mongoose.InferSchemaType<typeof TestSchema>, 'testField'>>())
```

Part of the problem is that `ret` is typed as `Record<string, any>` in transform options, which makes the return value of `transform()` not useful.
With #15485, you'll be able to do the following:

```ts
import mongoose from 'mongoose'

const TestSchema  = new mongoose.Schema({
  name: String,
  testField: {
    type: String,
    required: true,
    default: 'blah'
  }
})

function transform(doc: unknown, ret: mongoose.InferSchemaType<typeof TestSchema>) {
  const { testField, ...val } = ret;
  return val;
}

TestSchema.set('toJSON', { transform })

const TestModel = mongoose.model('Test', TestSchema)

const doc = new TestModel()

getTestField(doc.toJSON<ReturnType<typeof transform>>())

function getTestField(obj: { testField: string }) {
  return obj.testField
}
```

We will look more into automatic typing for `toJSON()`, but for now this is the best we can do.
I do see a minor issue with the way you're handling the transform function. You'd want the typing of `ret` to include fields added by mongoose, so it would need to include `__v`. Afaik, `mongoose.InferSchemaType<typeof TestSchema>` does not include that field.

We can add that manually of course, but is there a mongoose utility type that can do that for us? Maybe a utility type that gets the typing of `toJSON()` and uses that as the type of `ret`?
Not currently, although we're working on adding `__v` and `_id` to InferSchemaType output for Mongoose 9. For now, you'd need to do something like the following to get the default return type of `toJSON()` (we need to add another fix to make __v and _id show up in the following)

```ts
import mongoose from 'mongoose'

const TestSchema  = new mongoose.Schema({
  name: String,
  testField: {
    type: String,
    required: true,
    default: 'blah'
  }
})

type HydratedTestDoc = mongoose.HydratedDocumentFromSchema<typeof TestSchema>;

// Unused helper to allow TypeScript to get the return type of toJSON()
function toJSON(doc: HydratedTestDoc) {
  return doc.toJSON();
}

function transform(doc: unknown, ret: ReturnType<typeof toJSON>) {
  const { testField, ...val } = ret;
  return val;
}

TestSchema.set('toJSON', { transform })

const TestModel = mongoose.model('Test', TestSchema)

const doc = new TestModel()

getTestField(doc.toJSON<ReturnType<typeof transform>>())

function getTestField(obj: { testField: string }) {
  return obj.testField
}
```



Patch:
diff --git a/types/index.d.ts b/types/index.d.ts
index c50c7fbc850..15f70f27508 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -206,7 +206,7 @@ declare module 'mongoose' {
     [k: string]: string;
   }
 
-  export interface ToObjectOptions<THydratedDocumentType = HydratedDocument<unknown>> {
+  export interface ToObjectOptions<RawDocType = unknown, THydratedDocumentType = HydratedDocument<RawDocType>> {
     /** if `options.virtuals = true`, you can set `options.aliases = false` to skip applying aliases. This option is a no-op if `options.virtuals = false`. */
     aliases?: boolean;
     /** if true, replace any conventionally populated paths with the original id in the output. Has no affect on virtual populated paths. */
@@ -224,7 +224,7 @@ declare module 'mongoose' {
     /** if set, mongoose will call this function to allow you to transform the returned object */
     transform?: boolean | ((
       doc: THydratedDocumentType,
-      ret: Record<string, any>,
+      ret: RawDocType,
       options: ToObjectOptions<THydratedDocumentType>
     ) => any);
     /** If true, omits fields that are excluded in this document's projection. Unless you specified a projection, this will omit any field that has `select: false` in the schema. */
@@ -505,7 +505,7 @@ declare module 'mongoose' {
     requiredPaths(invalidate?: boolean): string[];
 
     /** Sets a schema option. */
-    set<K extends keyof SchemaOptions>(key: K, value: SchemaOptions[K], _tags?: any): this;
+    set<K extends keyof SchemaOptions>(key: K, value: SchemaOptions<DocType>[K], _tags?: any): this;
 
     /** Adds static "class" methods to Models compiled from this schema. */
     static<K extends keyof TStaticMethods>(name: K, fn: TStaticMethods[K]): this;
diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts
index 29552553297..02b9886bc21 100644
--- a/types/schemaoptions.d.ts
+++ b/types/schemaoptions.d.ts
@@ -146,14 +146,14 @@ declare module 'mongoose' {
      */
     strictQuery?: boolean | 'throw';
     /** Exactly the same as the toObject option but only applies when the document's toJSON method is called. */
-    toJSON?: ToObjectOptions<THydratedDocumentType>;
+    toJSON?: ToObjectOptions<DocType, THydratedDocumentType>;
     /**
      * Documents have a toObject method which converts the mongoose document into a plain JavaScript object.
      * This method accepts a few options. Instead of applying these options on a per-document basis, we may
      * declare the options at the schema level and have them applied to all of the schema's documents by
      * default.
      */
-    toObject?: ToObjectOptions<THydratedDocumentType>;
+    toObject?: ToObjectOptions<DocType, THydratedDocumentType>;
     /**
      * By default, if you have an object with key 'type' in your schema, mongoose will interpret it as a
      * type declaration. However, for applications like geoJSON, the 'type' property is important. If you want to


Test patch:
diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts
index bd3aaea80b8..1966081a50c 100644
--- a/test/types/schema.test.ts
+++ b/test/types/schema.test.ts
@@ -1812,3 +1812,32 @@ function defaultReturnsUndefined() {
     }
   });
 }
+
+function gh15479() {
+  const TestSchema = new Schema({
+    name: String,
+    testField: {
+      type: String,
+      required: true,
+      default: 'blah'
+    }
+  });
+
+  function transform(doc: unknown, ret: InferSchemaType<typeof TestSchema>) {
+    const { testField, ...val } = ret;
+    return val;
+  }
+
+  TestSchema.set('toJSON', { transform });
+
+  const TestModel = model('Test', TestSchema);
+
+  const doc = new TestModel();
+
+  getTestField(doc.toJSON<ReturnType<typeof transform> & { testField: string }>());
+  expectError(getTestField(doc.toJSON<ReturnType<typeof transform>>()));
+
+  function getTestField(obj: { testField: string }) {
+    return obj.testField;
+  }
+}