diff --git a/docs/migrating_to_9.md b/docs/migrating_to_9.md index 524cde9b4e..1854abc9c8 100644 --- a/docs/migrating_to_9.md +++ b/docs/migrating_to_9.md @@ -294,6 +294,26 @@ function findById(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` 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`, you can use `as` to cast as follows. + +```ts +const doc = await TestModel.create({ age: 'not a number', someOtherProperty: 'value' } as unknown as Partial>); +``` + ### Document `id` is no longer `any` In Mongoose 8 and earlier, `id` was a property on the `Document` class that was set to `any`. diff --git a/test/types/connection.test.ts b/test/types/connection.test.ts index 1e9b792d13..79954666aa 100644 --- a/test/types/connection.test.ts +++ b/test/types/connection.test.ts @@ -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'; @@ -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>); expectType(randomObject.userName); const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' }); diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 618ce84a1c..51ea1e8dda 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -48,10 +48,8 @@ Test.create([{}]).then(docs => { expectType(docs[0].name); }); -expectError(Test.create({})); - -Test.create({ name: 'test' }); -Test.create({ _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(docs[0]._id); @@ -137,4 +135,61 @@ async function createWithAggregateErrors() { expectType<(HydratedDocument | 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(doc.name); + expectType(doc.registeredAt); + expectType(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(doc.name); + expectType(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(doc.name); + expectType(doc.subdocMap!.get('taco')!.prop); + + const doc2 = await TestModel.create({ name: 'test', subdocMap: [['taco', { prop: 'beef' }]] }); + expectType(doc2.name); + expectType(doc2.subdocMap!.get('taco')!.prop); +} + +async function createWithRawDocTypeNo_id() { + interface RawDocType { + name: string; + registeredAt: Date; + } + + const schema = new Schema({ + name: String, + registeredAt: Date + }); + const TestModel = model('Test', schema); + + const doc = await TestModel.create({ _id: '0'.repeat(24), name: 'test' }); + expectType(doc.name); + expectType(doc._id); + + const doc2 = await TestModel.create({ name: 'test', _id: new Types.ObjectId() }); + expectType(doc2.name); + expectType(doc2._id); +} + createWithAggregateErrors(); diff --git a/test/types/document.test.ts b/test/types/document.test.ts index eb0857fe13..b4668fe104 100644 --- a/test/types/document.test.ts +++ b/test/types/document.test.ts @@ -137,7 +137,7 @@ async function gh11117(): Promise { const fooModel = model('foos', fooSchema); - const items = await fooModel.create([ + const items = await fooModel.create([ { someId: new Types.ObjectId(), someDate: new Date(), diff --git a/test/types/models.test.ts b/test/types/models.test.ts index f2e72bf8d7..5b5eb19461 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -330,8 +330,6 @@ async function gh12277() { } async function overwriteBulkWriteContents() { - type DocumentType = Document & T; - interface BaseModelClassDoc { firstname: string; } @@ -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>); expectType(randomObject.userName); const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' }); diff --git a/types/models.d.ts b/types/models.d.ts index 5dad52a994..9942d07cd3 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -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 = { + [K in keyof T]: NonNullable extends Map + ? (Record | Array<[KeyType, ValueType]> | T[K]) + : NonNullable extends Types.DocumentArray + ? RawSubdocType[] | T[K] + : QueryTypeCasting; + }; + /** * Models are fancy constructors compiled from `Schema` definitions. * An instance of a model is called a document. @@ -344,10 +362,10 @@ declare module 'mongoose' { >; /** Creates a new document or documents */ - create>(docs: Array, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; - create>(docs: Array, options?: CreateOptions): Promise; - create>(doc: DocContents | TRawDocType): Promise; - create>(...docs: Array): Promise; + create(docs: Array>>>, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; + create(docs: Array>>>, options?: CreateOptions): Promise; + create(doc: Partial>>): Promise; + create(...docs: Array>>>): Promise; /** * Create the collection for this model. By default, if no indexes are specified, @@ -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>(doc: DocContents | TRawDocType, options?: SaveOptions): Promise; + insertOne(doc: Partial>, options?: SaveOptions): Promise; /** * List all [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) on this model's collection. diff --git a/types/query.d.ts b/types/query.d.ts index 6d7c7e0774..eb56d36613 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -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 extends string @@ -13,7 +14,9 @@ declare module 'mongoose' { ? UUIDQueryTypeCasting : T extends Buffer ? BufferQueryCasting - : T; + : NonNullable extends Date + ? DateQueryTypeCasting | T + : T; export type ApplyBasicQueryCasting = T | T[] | (T extends (infer U)[] ? QueryTypeCasting : T);