Skip to content
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

BREAKING CHANGE: allow null for optional fields in TypeScript #13901

Merged
merged 4 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

why is null here but not undefined?

})();

(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
vkarpov15 marked this conversation as resolved.
Show resolved Hide resolved
}

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,13 +24,22 @@
* @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']> &

Check warning on line 35 in types/inferschematype.d.ts

View workflow job for this annotation

GitHub Actions / Lint TS-Files

Expected indentation of 6 spaces but found 8
OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)

Check warning on line 36 in types/inferschematype.d.ts

View workflow job for this annotation

GitHub Actions / Lint TS-Files

Expected indentation of 6 spaces but found 8
]: IsPathRequired<DocDefinition[K], TSchemaOptions['typeKey']> extends true ?
ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> :
ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> | null;
};

Check warning on line 40 in types/inferschematype.d.ts

View workflow job for this annotation

GitHub Actions / Lint TS-Files

Expected indentation of 4 spaces but found 3

/**

Check warning on line 42 in types/inferschematype.d.ts

View workflow job for this annotation

GitHub Actions / Lint TS-Files

Expected indentation of 2 spaces but found 3
* @summary Obtains document schema type from Schema instance.
* @param {Schema} TSchema `typeof` a schema instance.
* @example
Expand All @@ -39,7 +48,7 @@
* // result
* type UserType = {userName?: string}
*/
export type InferSchemaType<TSchema> = IfAny<TSchema, any, ObtainSchemaGeneric<TSchema, 'DocType'>>;

Check warning on line 51 in types/inferschematype.d.ts

View workflow job for this annotation

GitHub Actions / Lint TS-Files

Expected indentation of 2 spaces but found 3

/**
* @summary Obtains schema Generic type by using generic alias.
Expand Down