Skip to content

BREAKING CHANGE: make create() and insertOne() params more strict, remove generics to prevent type inference #15587

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

Open
wants to merge 3 commits into
base: 9.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/migrating_to_9.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,26 @@ function findById<ModelType extends {_id: Types.ObjectId | string}>(model: Model
}
```

### No more generic parameter for `create()` and `insertOne()`

In Mongoose 8, `create()` and `insertOne()` accepted a generic parameter, which meant TypeScript let you pass any value to the function.

```ts
const schema = new Schema({ age: Number });
const TestModel = mongoose.model('Test', schema);

// Worked in Mongoose 8, TypeScript error in Mongoose 9
const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' });
```

In Mongoose 9, `create()` and `insertOne()` no longer accept a generic parameter. Instead, they accept `Partial<RawDocType>` with some additional query casting applied that allows objects for maps, strings for ObjectIds, and POJOs for subdocuments and document arrays.

If your parameters to `create()` don't match `Partial<RawDocType>`, you can use `as` to cast as follows.

```ts
const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial<InferSchemaType<typeof schema>>);
Copy link
Preview

Copilot AI Aug 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example uses a double type assertion (as unknown as) which is generally considered a code smell. Consider showing a cleaner alternative or explaining why this specific pattern is necessary.

Suggested change
const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial<InferSchemaType<typeof schema>>);
// Prefer defining the object with the correct type to avoid double assertion:
const data: Partial<InferSchemaType<typeof schema>> = { age: 'not a number', someOtherProperty: 'value' };
const doc = await TestModel.create(data);
// If you must use a type assertion, use a single assertion if possible:
// const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as Partial<InferSchemaType<typeof schema>>);
// If TypeScript still complains and you must use a double assertion, be aware this bypasses type safety:
// const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial<InferSchemaType<typeof schema>>);

Copilot uses AI. Check for mistakes.

```

### Document `id` is no longer `any`

In Mongoose 8 and earlier, `id` was a property on the `Document` class that was set to `any`.
Expand Down
4 changes: 2 additions & 2 deletions test/types/connection.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createConnection, Schema, Collection, Connection, ConnectionSyncIndexesResult, Model, connection, HydratedDocument, Query } from 'mongoose';
import { createConnection, Schema, Collection, Connection, ConnectionSyncIndexesResult, InferSchemaType, Model, connection, HydratedDocument, Query } from 'mongoose';
import * as mongodb from 'mongodb';
import { expectAssignable, expectError, expectType } from 'tsd';
import { AutoTypedSchemaType, autoTypedSchema } from './schema.test';
Expand Down Expand Up @@ -93,7 +93,7 @@ export function autoTypedModelConnection() {
(async() => {
// Model-functions-test
// Create should works with arbitrary objects.
const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' });
const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' } as Partial<InferSchemaType<typeof AutoTypedSchema>>);
expectType<AutoTypedSchemaType['schema']['userName']>(randomObject.userName);

const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' });
Expand Down
63 changes: 59 additions & 4 deletions test/types/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,8 @@ Test.create([{}]).then(docs => {
expectType<string>(docs[0].name);
});

expectError(Test.create<ITest>({}));

Test.create<ITest>({ name: 'test' });
Test.create<ITest>({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' });
Test.create({ name: 'test' });
Test.create({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' });

Test.insertMany({ name: 'test' }, {}).then(docs => {
expectType<Types.ObjectId>(docs[0]._id);
Expand Down Expand Up @@ -137,4 +135,61 @@ async function createWithAggregateErrors() {
expectType<(HydratedDocument<ITest> | Error)[]>(await Test.create([{}], { aggregateErrors: true }));
}

async function createWithSubdoc() {
const schema = new Schema({ name: String, registeredAt: Date, subdoc: new Schema({ prop: { type: String, required: true } }) });
const TestModel = model('Test', schema);
const doc = await TestModel.create({ name: 'test', registeredAt: '2022-06-01', subdoc: { prop: 'value' } });
expectType<string | null | undefined>(doc.name);
expectType<Date | null | undefined>(doc.registeredAt);
expectType<string>(doc.subdoc!.prop);
}

async function createWithDocArray() {
const schema = new Schema({ name: String, subdocs: [new Schema({ prop: { type: String, required: true } })] });
const TestModel = model('Test', schema);
const doc = await TestModel.create({ name: 'test', subdocs: [{ prop: 'value' }] });
expectType<string | null | undefined>(doc.name);
expectType<string>(doc.subdocs[0].prop);
}

async function createWithMapOfSubdocs() {
const schema = new Schema({
name: String,
subdocMap: {
type: Map,
of: new Schema({ prop: { type: String, required: true } })
}
});
const TestModel = model('Test', schema);

const doc = await TestModel.create({ name: 'test', subdocMap: { taco: { prop: 'beef' } } });
expectType<string | null | undefined>(doc.name);
expectType<string>(doc.subdocMap!.get('taco')!.prop);

const doc2 = await TestModel.create({ name: 'test', subdocMap: [['taco', { prop: 'beef' }]] });
expectType<string | null | undefined>(doc2.name);
expectType<string>(doc2.subdocMap!.get('taco')!.prop);
}

async function createWithRawDocTypeNo_id() {
interface RawDocType {
name: string;
registeredAt: Date;
}

const schema = new Schema<RawDocType>({
name: String,
registeredAt: Date
});
const TestModel = model<RawDocType>('Test', schema);

const doc = await TestModel.create({ _id: '0'.repeat(24), name: 'test' });
expectType<string>(doc.name);
expectType<Types.ObjectId>(doc._id);

const doc2 = await TestModel.create({ name: 'test', _id: new Types.ObjectId() });
expectType<string>(doc2.name);
expectType<Types.ObjectId>(doc2._id);
}

createWithAggregateErrors();
2 changes: 1 addition & 1 deletion test/types/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ async function gh11117(): Promise<void> {

const fooModel = model('foos', fooSchema);

const items = await fooModel.create<Foo>([
const items = await fooModel.create([
{
someId: new Types.ObjectId(),
someDate: new Date(),
Expand Down
4 changes: 1 addition & 3 deletions test/types/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,6 @@ async function gh12277() {
}

async function overwriteBulkWriteContents() {
type DocumentType<T> = Document<any, any, T> & T;

interface BaseModelClassDoc {
firstname: string;
}
Expand Down Expand Up @@ -380,7 +378,7 @@ export function autoTypedModel() {
(async() => {
// Model-functions-test
// Create should works with arbitrary objects.
const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' });
const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' } as Partial<InferSchemaType<typeof AutoTypedSchema>>);
expectType<AutoTypedSchemaType['schema']['userName']>(randomObject.userName);

const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' });
Expand Down
28 changes: 23 additions & 5 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,24 @@ declare module 'mongoose' {
hint?: mongodb.Hint;
}

/*
* Apply common casting logic to the given type, allowing:
* - strings for ObjectIds
* - strings and numbers for Dates
* - strings for Buffers
* - strings for UUIDs
* - POJOs for subdocuments
* - vanilla arrays of POJOs for document arrays
* - POJOs and array of arrays for maps
*/
type ApplyBasicCreateCasting<T> = {
[K in keyof T]: NonNullable<T[K]> extends Map<infer KeyType extends string, infer ValueType>
? (Record<KeyType, ValueType> | Array<[KeyType, ValueType]> | T[K])
: NonNullable<T[K]> extends Types.DocumentArray<infer RawSubdocType>
? RawSubdocType[] | T[K]
: QueryTypeCasting<T[K]>;
};

/**
* Models are fancy constructors compiled from `Schema` definitions.
* An instance of a model is called a document.
Expand Down Expand Up @@ -344,10 +362,10 @@ declare module 'mongoose' {
>;

/** Creates a new document or documents */
create<DocContents = AnyKeys<TRawDocType>>(docs: Array<TRawDocType | DocContents>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>;
create<DocContents = AnyKeys<TRawDocType>>(docs: Array<TRawDocType | DocContents>, options?: CreateOptions): Promise<THydratedDocumentType[]>;
create<DocContents = AnyKeys<TRawDocType>>(doc: DocContents | TRawDocType): Promise<THydratedDocumentType>;
create<DocContents = AnyKeys<TRawDocType>>(...docs: Array<TRawDocType | DocContents>): Promise<THydratedDocumentType[]>;
create(docs: Array<Partial<ApplyBasicCreateCasting<Require_id<TRawDocType>>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>;
create(docs: Array<Partial<ApplyBasicCreateCasting<Require_id<TRawDocType>>>>, options?: CreateOptions): Promise<THydratedDocumentType[]>;
create(doc: Partial<ApplyBasicCreateCasting<Require_id<TRawDocType>>>): Promise<THydratedDocumentType>;
create(...docs: Array<Partial<ApplyBasicCreateCasting<Require_id<TRawDocType>>>>): Promise<THydratedDocumentType[]>;

/**
* Create the collection for this model. By default, if no indexes are specified,
Expand Down Expand Up @@ -616,7 +634,7 @@ declare module 'mongoose' {
* `MyModel.insertOne(obj, options)` is almost equivalent to `new MyModel(obj).save(options)`.
* The difference is that `insertOne()` checks if `obj` is already a document, and checks for discriminators.
*/
insertOne<DocContents = AnyKeys<TRawDocType>>(doc: DocContents | TRawDocType, options?: SaveOptions): Promise<THydratedDocumentType>;
insertOne(doc: Partial<ApplyBasicCreateCasting<TRawDocType>>, options?: SaveOptions): Promise<THydratedDocumentType>;

/**
* List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection.
Expand Down
5 changes: 4 additions & 1 deletion types/query.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ declare module 'mongoose' {

type StringQueryTypeCasting = string | RegExp;
type ObjectIdQueryTypeCasting = Types.ObjectId | string;
type DateQueryTypeCasting = string | number;
type UUIDQueryTypeCasting = Types.UUID | string;
type BufferQueryCasting = Buffer | mongodb.Binary | number[] | string | { $binary: string | mongodb.Binary };
type QueryTypeCasting<T> = T extends string
Expand All @@ -13,7 +14,9 @@ declare module 'mongoose' {
? UUIDQueryTypeCasting
: T extends Buffer
? BufferQueryCasting
: T;
: NonNullable<T> extends Date
? DateQueryTypeCasting | T
: T;

export type ApplyBasicQueryCasting<T> = T | T[] | (T extends (infer U)[] ? QueryTypeCasting<U> : T);

Expand Down