From 905b28bc1fce3eff7cd329ee5a0fa560bf9c94c6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Sep 2023 16:51:35 -0400 Subject: [PATCH 1/3] BREAKING CHANGE: allow `null` for optional fields in TypeScript Fix #12748 Re: #12781 --- test/types/models.test.ts | 4 +- test/types/queries.test.ts | 7 +- test/types/querycursor.test.ts | 2 +- test/types/schema.test.ts | 120 ++++++++++++++++----------------- types/inferschematype.d.ts | 17 +++-- 5 files changed, 80 insertions(+), 70 deletions(-) diff --git a/test/types/models.test.ts b/test/types/models.test.ts index a53404c5c00..b45544201a7 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -477,7 +477,7 @@ function gh12100() { const TestModel = model('test', schema_with_string_id); const obj = new TestModel(); - expectType(obj._id); + expectType(obj._id); })(); (async function gh12094() { @@ -644,7 +644,7 @@ async function gh13705() { const schema = new Schema({ name: String }); const TestModel = model('Test', schema); - type ExpectedLeanDoc = (mongoose.FlattenMaps<{ name?: string }> & { _id: mongoose.Types.ObjectId }); + type ExpectedLeanDoc = (mongoose.FlattenMaps<{ name?: string | null }> & { _id: mongoose.Types.ObjectId }); const findByIdRes = await TestModel.findById('0'.repeat(24), undefined, { lean: true }); expectType(findByIdRes); diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index ff5f7c18cba..9c81551c2c1 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -393,7 +393,8 @@ async function gh12342_manual() { async function gh12342_auto() { interface Project { - name?: string, stars?: number + name?: string | null, + stars?: number | null } const ProjectSchema = new Schema({ @@ -483,8 +484,8 @@ async function gh13224() { const UserModel = model('User', userSchema); const u1 = await UserModel.findOne().select(['name']).orFail(); - expectType(u1.name); - expectType(u1.age); + expectType(u1.name); + expectType(u1.age); expectAssignable(u1.toObject); const u2 = await UserModel.findOne().select<{ name?: string }>(['name']).orFail(); diff --git a/test/types/querycursor.test.ts b/test/types/querycursor.test.ts index 611e9543977..dc87e0a669b 100644 --- a/test/types/querycursor.test.ts +++ b/test/types/querycursor.test.ts @@ -10,7 +10,7 @@ type ITest = ReturnType<(typeof Test)['hydrate']>; Test.find().cursor(). eachAsync(async(doc: ITest) => { expectType(doc._id); - expectType(doc.name); + expectType(doc.name); }). then(() => console.log('Done!')); diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index fc0204e2a71..c0abc84a031 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -372,50 +372,50 @@ export function autoTypedSchema() { } type TestSchemaType = { - string1?: string; - string2?: string; - string3?: string; - string4?: string; + string1?: string | null; + string2?: string | null; + string3?: string | null; + string4?: string | null; string5: string; - number1?: number; - number2?: number; - number3?: number; - number4?: number; + number1?: number | null; + number2?: number | null; + number3?: number | null; + number4?: number | null; number5: number; - date1?: Date; - date2?: Date; - date3?: Date; - date4?: Date; + date1?: Date | null; + date2?: Date | null; + date3?: Date | null; + date4?: Date | null; date5: Date; - buffer1?: Buffer; - buffer2?: Buffer; - buffer3?: Buffer; - buffer4?: Buffer; - boolean1?: boolean; - boolean2?: boolean; - boolean3?: boolean; - boolean4?: boolean; + buffer1?: Buffer | null; + buffer2?: Buffer | null; + buffer3?: Buffer | null; + buffer4?: Buffer | null; + boolean1?: boolean | null; + boolean2?: boolean | null; + boolean3?: boolean | null; + boolean4?: boolean | null; boolean5: boolean; - mixed1?: any; - mixed2?: any; - mixed3?: any; - objectId1?: Types.ObjectId; - objectId2?: Types.ObjectId; - objectId3?: Types.ObjectId; - customSchema?: Int8; - map1?: Map; - map2?: Map; + mixed1?: any | null; + mixed2?: any | null; + mixed3?: any | null; + objectId1?: Types.ObjectId | null; + objectId2?: Types.ObjectId | null; + objectId3?: Types.ObjectId | null; + customSchema?: Int8 | null; + map1?: Map | null; + map2?: Map | null; array1: string[]; array2: any[]; array3: any[]; array4: any[]; array5: any[]; array6: string[]; - array7?: string[]; - array8?: string[]; - decimal1?: Types.Decimal128; - decimal2?: Types.Decimal128; - decimal3?: Types.Decimal128; + array7?: string[] | null; + array8?: string[] | null; + decimal1?: Types.Decimal128 | null; + decimal2?: Types.Decimal128 | null; + decimal3?: Types.Decimal128 | null; }; const TestSchema = new Schema({ @@ -546,17 +546,17 @@ export function autoTypedSchema() { export type AutoTypedSchemaType = { schema: { userName: string; - description?: string; + description?: string | null; nested?: { age: number; - hobby?: string - }, - favoritDrink?: 'Tea' | 'Coffee', + hobby?: string | null + } | null, + favoritDrink?: 'Tea' | 'Coffee' | null, favoritColorMode: 'dark' | 'light' - friendID?: Types.ObjectId; + friendID?: Types.ObjectId | null; nestedArray: Types.DocumentArray<{ date: Date; - messages?: number; + messages?: number | null; }> } , statics: { @@ -634,7 +634,7 @@ function gh12003() { type TSchemaOptions = ResolveSchemaOptions>; expectType<'type'>({} as TSchemaOptions['typeKey']); - expectType<{ name?: string }>({} as BaseSchemaType); + expectType<{ name?: string | null }>({} as BaseSchemaType); } function gh11987() { @@ -670,7 +670,7 @@ function gh12030() { } ]>; expectType<{ - username?: string + username?: string | null }[]>({} as A); type B = ObtainDocumentType<{ @@ -682,13 +682,13 @@ function gh12030() { }>; expectType<{ users: { - username?: string + username?: string | null }[]; }>({} as B); expectType<{ users: { - username?: string + username?: string | null }[]; }>({} as InferSchemaType); @@ -710,7 +710,7 @@ function gh12030() { expectType<{ users: Types.DocumentArray<{ credit: number; - username?: string; + username?: string | null; }>; }>({} as InferSchemaType); @@ -719,7 +719,7 @@ function gh12030() { data: { type: { role: String }, default: {} } }); - expectType<{ data: { role?: string } }>({} as InferSchemaType); + expectType<{ data: { role?: string | null } }>({} as InferSchemaType); const Schema5 = new Schema({ data: { type: { role: Object }, default: {} } @@ -744,7 +744,7 @@ function gh12030() { track?: { backupCount: number; count: number; - }; + } | null; }>({} as InferSchemaType); } @@ -821,7 +821,7 @@ function gh12450() { }); expectType<{ - user?: Types.ObjectId; + user?: Types.ObjectId | null; }>({} as InferSchemaType); const Schema2 = new Schema({ @@ -836,14 +836,14 @@ function gh12450() { decimalValue: { type: Schema.Types.Decimal128 } }); - expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType); + expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType); const Schema4 = new Schema({ createdAt: { type: Date }, decimalValue: { type: Schema.Types.Decimal128 } }); - expectType<{ createdAt?: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType); + expectType<{ createdAt?: Date | null, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType); } function gh12242() { @@ -867,13 +867,13 @@ function testInferTimestamps() { // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & // { name?: string | undefined; }" - expectType<{ createdAt: Date, updatedAt: Date } & { name?: string }>({} as WithTimestamps); + expectType<{ createdAt: Date, updatedAt: Date } & { name?: string | null }>({} as WithTimestamps); const schema2 = new Schema({ name: String }, { timestamps: true, - methods: { myName(): string | undefined { + methods: { myName(): string | undefined | null { return this.name; } } }); @@ -883,7 +883,7 @@ function testInferTimestamps() { // an error "Parameter type { createdAt: Date; updatedAt: Date; name?: string | undefined; } // is not identical to argument type { createdAt: NativeDate; updatedAt: NativeDate; } & // { name?: string | undefined; }" - expectType<{ name?: string }>({} as WithTimestamps2); + expectType<{ name?: string | null }>({} as WithTimestamps2); } function gh12431() { @@ -893,25 +893,25 @@ function gh12431() { }); type Example = InferSchemaType; - expectType<{ testDate?: Date, testDecimal?: Types.Decimal128 }>({} as Example); + expectType<{ testDate?: Date | null, testDecimal?: Types.Decimal128 | null }>({} as Example); } async function gh12593() { const testSchema = new Schema({ x: { type: Schema.Types.UUID } }); type Example = InferSchemaType; - expectType<{ x?: Buffer }>({} as Example); + expectType<{ x?: Buffer | null }>({} as Example); const Test = model('Test', testSchema); const doc = await Test.findOne({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }).orFail(); - expectType(doc.x); + expectType(doc.x); const doc2 = new Test({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' }); - expectType(doc2.x); + expectType(doc2.x); const doc3 = await Test.findOne({}).orFail().lean(); - expectType(doc3.x); + expectType(doc3.x); const arrSchema = new Schema({ arr: [{ type: Schema.Types.UUID }] }); @@ -978,7 +978,7 @@ function gh12611() { expectType<{ description: string; skills: Types.ObjectId[]; - anotherField?: string; + anotherField?: string | null; }>({} as Props); } @@ -1181,7 +1181,7 @@ function gh13702() { function gh13780() { const schema = new Schema({ num: Schema.Types.BigInt }); type InferredType = InferSchemaType; - expectType(null as unknown as InferredType['num']); + expectType(null as unknown as InferredType['num']); } function gh13800() { diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 75240ed1675..256c4f65db3 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -24,10 +24,19 @@ declare module 'mongoose' { * @param {EnforcedDocType} EnforcedDocType A generic type enforced by user "provided before schema constructor". * @param {TypeKey} TypeKey A generic of literal string type."Refers to the property used for path type definition". */ - type ObtainDocumentType = DefaultSchemaOptions> = - IsItRecordAndNotAny extends true ? EnforcedDocType : { - [K in keyof (RequiredPaths & - OptionalPaths)]: ObtainDocumentPathType; + type ObtainDocumentType< + DocDefinition, + EnforcedDocType = any, + TSchemaOptions extends Record = DefaultSchemaOptions + > = IsItRecordAndNotAny extends true ? + EnforcedDocType : + { + [ + K in keyof (RequiredPaths & + OptionalPaths) + ]: IsPathRequired extends true ? + ObtainDocumentPathType : + ObtainDocumentPathType | null; }; /** From 5cbe56305153799870156180ae78c7df46cadfa3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 26 Sep 2023 17:00:46 -0400 Subject: [PATCH 2/3] docs(migrating_to_8): add note about `null` for optional keys in TypeScript --- docs/migrating_to_8.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md index b9c8cf1346a..bf3856c435d 100644 --- a/docs/migrating_to_8.md +++ b/docs/migrating_to_8.md @@ -16,6 +16,7 @@ If you're still on Mongoose 6.x or earlier, please read the [Mongoose 6.x to 7.x * [MongoDB Node Driver 6.0](#mongodb-node-driver-6) * [Removed `findOneAndRemove()`](#removed-findoneandremove) * [Removed id Setter](#removed-id-setter) +* [Allow `null` For Optional Fields in TypeScript](#allow-null-for-optional-fields-in-typescript)

Removed rawResult option for findOneAndUpdate()

@@ -59,4 +60,20 @@ Use `findOneAndDelete()` instead.

Removed id Setter

In Mongoose 7.4, Mongoose introduced an `id` setter that made `doc.id = '0'.repeat(24)` equivalent to `doc._id = '0'.repeat(24)`. -In Mongoose 8, that setter is now removed. \ No newline at end of file +In Mongoose 8, that setter is now removed. + +

Allow null For Optional Fields in TypeScript

+ +In Mongoose 8, automatically inferred schema types in TypeScript allow `null` for optional fields. +In Mongoose 7, optional fields only allowed `undefined`, not `null`. + +```typescript +const schema = new Schema({ name: String }); +const TestModel = model('Test', schema); + +const doc = new TestModel(); + +// In Mongoose 8, this type is `string | null | undefined`. +// In Mongoose 7, this type is `string | undefined` +doc.name; +``` \ No newline at end of file From 9aa8cdf8a2525dfabf074a93a97ce0ed2ff5c23d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 28 Sep 2023 11:13:06 -0400 Subject: [PATCH 3/3] Update docs/migrating_to_8.md Co-authored-by: hasezoey --- docs/migrating_to_8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md index bf3856c435d..788ebe92da3 100644 --- a/docs/migrating_to_8.md +++ b/docs/migrating_to_8.md @@ -76,4 +76,4 @@ const doc = new TestModel(); // In Mongoose 8, this type is `string | null | undefined`. // In Mongoose 7, this type is `string | undefined` doc.name; -``` \ No newline at end of file +```