Skip to content

Commit

Permalink
Merge pull request #13901 from Automattic/vkarpov15/gh-12748
Browse files Browse the repository at this point in the history
BREAKING CHANGE: allow null for optional fields in TypeScript
  • Loading branch information
vkarpov15 committed Oct 4, 2023
2 parents 73f9412 + 0cc0ac3 commit f7afc96
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 70 deletions.
17 changes: 17 additions & 0 deletions docs/migrating_to_8.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<h2 id="removed-rawresult-option-for-findoneandupdate"><a href="#removed-rawresult-option-for-findoneandupdate">Removed <code>rawResult</code> option for <code>findOneAndUpdate()</code></a></h2>

Expand Down Expand Up @@ -60,3 +61,19 @@ Use `findOneAndDelete()` instead.

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.

<h2 id="allow-null-for-optional-fields-in-typescript"><a href="#allow-null-for-optional-fields-in-typescript">Allow <code>null</code> For Optional Fields in TypeScript</a></h2>

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;
```
4 changes: 2 additions & 2 deletions test/types/models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ function gh12100() {
const TestModel = model('test', schema_with_string_id);
const obj = new TestModel();

expectType<string>(obj._id);
expectType<string | null>(obj._id);
})();

(async function gh12094() {
Expand Down Expand Up @@ -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<ExpectedLeanDoc | null>(findByIdRes);
Expand Down
7 changes: 4 additions & 3 deletions test/types/queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -483,8 +484,8 @@ async function gh13224() {
const UserModel = model('User', userSchema);

const u1 = await UserModel.findOne().select(['name']).orFail();
expectType<string | undefined>(u1.name);
expectType<number | undefined>(u1.age);
expectType<string | undefined | null>(u1.name);
expectType<number | undefined | null>(u1.age);
expectAssignable<Function>(u1.toObject);

const u2 = await UserModel.findOne().select<{ name?: string }>(['name']).orFail();
Expand Down
2 changes: 1 addition & 1 deletion test/types/querycursor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type ITest = ReturnType<(typeof Test)['hydrate']>;
Test.find().cursor().
eachAsync(async(doc: ITest) => {
expectType<Types.ObjectId>(doc._id);
expectType<string | undefined>(doc.name);
expectType<string | undefined | null>(doc.name);
}).
then(() => console.log('Done!'));

Expand Down
120 changes: 60 additions & 60 deletions test/types/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
map2?: Map<string, number>;
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<string, string> | null;
map2?: Map<string, number> | 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({
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -634,7 +634,7 @@ function gh12003() {
type TSchemaOptions = ResolveSchemaOptions<ObtainSchemaGeneric<typeof BaseSchema, 'TSchemaOptions'>>;
expectType<'type'>({} as TSchemaOptions['typeKey']);

expectType<{ name?: string }>({} as BaseSchemaType);
expectType<{ name?: string | null }>({} as BaseSchemaType);
}

function gh11987() {
Expand Down Expand Up @@ -670,7 +670,7 @@ function gh12030() {
}
]>;
expectType<{
username?: string
username?: string | null
}[]>({} as A);

type B = ObtainDocumentType<{
Expand All @@ -682,13 +682,13 @@ function gh12030() {
}>;
expectType<{
users: {
username?: string
username?: string | null
}[];
}>({} as B);

expectType<{
users: {
username?: string
username?: string | null
}[];
}>({} as InferSchemaType<typeof Schema1>);

Expand All @@ -710,7 +710,7 @@ function gh12030() {
expectType<{
users: Types.DocumentArray<{
credit: number;
username?: string;
username?: string | null;
}>;
}>({} as InferSchemaType<typeof Schema3>);

Expand All @@ -719,7 +719,7 @@ function gh12030() {
data: { type: { role: String }, default: {} }
});

expectType<{ data: { role?: string } }>({} as InferSchemaType<typeof Schema4>);
expectType<{ data: { role?: string | null } }>({} as InferSchemaType<typeof Schema4>);

const Schema5 = new Schema({
data: { type: { role: Object }, default: {} }
Expand All @@ -744,7 +744,7 @@ function gh12030() {
track?: {
backupCount: number;
count: number;
};
} | null;
}>({} as InferSchemaType<typeof Schema6>);

}
Expand Down Expand Up @@ -821,7 +821,7 @@ function gh12450() {
});

expectType<{
user?: Types.ObjectId;
user?: Types.ObjectId | null;
}>({} as InferSchemaType<typeof ObjectIdSchema>);

const Schema2 = new Schema({
Expand All @@ -836,14 +836,14 @@ function gh12450() {
decimalValue: { type: Schema.Types.Decimal128 }
});

expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType<typeof Schema3>);
expectType<{ createdAt: Date, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType<typeof Schema3>);

const Schema4 = new Schema({
createdAt: { type: Date },
decimalValue: { type: Schema.Types.Decimal128 }
});

expectType<{ createdAt?: Date, decimalValue?: Types.Decimal128 }>({} as InferSchemaType<typeof Schema4>);
expectType<{ createdAt?: Date | null, decimalValue?: Types.Decimal128 | null }>({} as InferSchemaType<typeof Schema4>);
}

function gh12242() {
Expand All @@ -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;
} }
});
Expand All @@ -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() {
Expand All @@ -893,25 +893,25 @@ function gh12431() {
});

type Example = InferSchemaType<typeof testSchema>;
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<typeof testSchema>;
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<Buffer | undefined>(doc.x);
expectType<Buffer | undefined | null>(doc.x);

const doc2 = new Test({ x: '4709e6d9-61fd-435e-b594-d748eb196d8f' });
expectType<Buffer | undefined>(doc2.x);
expectType<Buffer | undefined | null>(doc2.x);

const doc3 = await Test.findOne({}).orFail().lean();
expectType<Buffer | undefined>(doc3.x);
expectType<Buffer | undefined | null>(doc3.x);

const arrSchema = new Schema({ arr: [{ type: Schema.Types.UUID }] });

Expand Down Expand Up @@ -978,7 +978,7 @@ function gh12611() {
expectType<{
description: string;
skills: Types.ObjectId[];
anotherField?: string;
anotherField?: string | null;
}>({} as Props);
}

Expand Down Expand Up @@ -1181,7 +1181,7 @@ function gh13702() {
function gh13780() {
const schema = new Schema({ num: Schema.Types.BigInt });
type InferredType = InferSchemaType<typeof schema>;
expectType<bigint | undefined>(null as unknown as InferredType['num']);
expectType<bigint | undefined | null>(null as unknown as InferredType['num']);
}

function gh13800() {
Expand Down
17 changes: 13 additions & 4 deletions types/inferschematype.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DocDefinition, EnforcedDocType = any, TSchemaOptions extends Record<any, any> = DefaultSchemaOptions> =
IsItRecordAndNotAny<EnforcedDocType> extends true ? EnforcedDocType : {
[K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)]: ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
type ObtainDocumentType<
DocDefinition,
EnforcedDocType = any,
TSchemaOptions extends Record<any, any> = DefaultSchemaOptions
> = IsItRecordAndNotAny<EnforcedDocType> extends true ?
EnforcedDocType :
{
[
K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)
]: IsPathRequired<DocDefinition[K], TSchemaOptions['typeKey']> extends true ?
ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> :
ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> | null;
};

/**
Expand Down

0 comments on commit f7afc96

Please sign in to comment.