From 0b9dc6f8ee67a5cbc8915aa38bf26b0edadb4141 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Tue, 8 Mar 2022 02:39:29 +0100 Subject: [PATCH 1/7] fix populate type narrowing --- test/types/populate.test.ts | 34 +++++++++++++++++++++++++++++++++- test/types/queries.test.ts | 2 +- types/index.d.ts | 5 ++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 5a3981b929c..0ad6007a20f 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -1,7 +1,7 @@ import { Schema, model, Document, PopulatedDoc, Types, HydratedDocument } from 'mongoose'; // Use the mongodb ObjectId to make instanceof calls possible import { ObjectId } from 'mongodb'; -import { expectError } from 'tsd'; +import { expectError, expectType } from 'tsd'; interface Child { name: string; @@ -149,4 +149,36 @@ function gh11321(): void { return 'foo'; } }); +} + +function gh11496() { + interface Friend { + blocked: boolean + } + const FriendSchema = new Schema({ + blocked: Boolean + }); + const Friends = model('friends', FriendSchema); + + + interface User { + friends: Types.ObjectId[]; + } + const UserSchema = new Schema({ + friends: [{ type: Schema.Types.ObjectId, ref: 'friends' }] + }); + const Users = model('friends', UserSchema); + + + Users.findOne({}).populate('friends').then(user => { + expectType(user?.friends[0]); + expectError(user?.friends[0].blocked); + expectError(user?.friends.map(friend => friend.blocked)); + }); + + Users.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { + expectType(user?.friends[0]); + user?.friends[0].blocked; // works + user?.friends.map(friend => friend.blocked); // Property 'blocked' does not exist on type 'ObjectId'.ts(2339) + }); } \ No newline at end of file diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 79e56648d6c..3638cd6f4e4 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -231,7 +231,7 @@ async function gh11041(): Promise { // 3. Create a Model. const MyModel = model('User', schema); - expectType | null>(await MyModel.findOne({}).populate('someField').exec()); + expectType, never> | null>(await MyModel.findOne({}).populate('someField').exec()); } async function gh11306(): Promise { diff --git a/types/index.d.ts b/types/index.d.ts index 29f89ad24de..031eb927910 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1979,7 +1979,10 @@ declare module 'mongoose' { type QueryWithHelpers = Query & THelpers; - type UnpackedIntersection = T extends (infer V)[] ? (V & U)[] : T & U; + type UnpackedIntersection = T extends (infer V)[] + ? UnpackedIntersectionMerge[] + : UnpackedIntersectionMerge; + type UnpackedIntersectionMerge = T extends AnyObject ? Omit & U: T & U; type UnpackedIntersectionWithNull = T extends null ? UnpackedIntersection | null : UnpackedIntersection; type ProjectionFields = {[Key in keyof Omit, '__v'>]?: any} & Record; From 1a01dfdd41c64ad316d130ceddce5fdba850c4d7 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Tue, 8 Mar 2022 15:54:54 +0100 Subject: [PATCH 2/7] Fix populate type narrowing issue --- test/types/populate.test.ts | 10 ++++------ test/types/queries.test.ts | 6 +++--- types/index.d.ts | 6 ++---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 0ad6007a20f..18ccab794bd 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -1,7 +1,7 @@ import { Schema, model, Document, PopulatedDoc, Types, HydratedDocument } from 'mongoose'; // Use the mongodb ObjectId to make instanceof calls possible import { ObjectId } from 'mongodb'; -import { expectError, expectType } from 'tsd'; +import { expectAssignable, expectError, expectType } from 'tsd'; interface Child { name: string; @@ -158,8 +158,6 @@ function gh11496() { const FriendSchema = new Schema({ blocked: Boolean }); - const Friends = model('friends', FriendSchema); - interface User { friends: Types.ObjectId[]; @@ -177,8 +175,8 @@ function gh11496() { }); Users.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { - expectType(user?.friends[0]); - user?.friends[0].blocked; // works - user?.friends.map(friend => friend.blocked); // Property 'blocked' does not exist on type 'ObjectId'.ts(2339) + expectAssignable(user?.friends[0]); + user?.friends[0].blocked; + user?.friends.map(friend => friend.blocked); }); } \ No newline at end of file diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 3638cd6f4e4..5ff1ba2bacf 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -18,11 +18,11 @@ const schema: Schema, {}, QueryHelpers> = new endDate: Date }); -schema.query._byName = function(name: string): QueryWithHelpers { +schema.query._byName = function(name: string): QueryWithHelpers { return this.find({ name }); }; -schema.query.byName = function(name: string): QueryWithHelpers { +schema.query.byName = function(name: string): QueryWithHelpers { expectError(this.notAQueryHelper()); return this._byName(name); }; @@ -231,7 +231,7 @@ async function gh11041(): Promise { // 3. Create a Model. const MyModel = model('User', schema); - expectType, never> | null>(await MyModel.findOne({}).populate('someField').exec()); + expectType | null>(await MyModel.findOne({}).populate('someField').exec()); } async function gh11306(): Promise { diff --git a/types/index.d.ts b/types/index.d.ts index 031eb927910..2a6ffb3ac48 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1979,10 +1979,8 @@ declare module 'mongoose' { type QueryWithHelpers = Query & THelpers; - type UnpackedIntersection = T extends (infer V)[] - ? UnpackedIntersectionMerge[] - : UnpackedIntersectionMerge; - type UnpackedIntersectionMerge = T extends AnyObject ? Omit & U: T & U; + type UnpackedIntersection = T extends (infer A)[] ? (A & U)[] : keyof U extends never ? T & U: { [P in keyof U]: U[P] extends (infer B)[] ? (B & T)[] : U[P] & T }; + type UnpackedIntersectionWithNull = T extends null ? UnpackedIntersection | null : UnpackedIntersection; type ProjectionFields = {[Key in keyof Omit, '__v'>]?: any} & Record; From ebbebd71bae86db8aea3d160fa6452e4432f7d6d Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 00:49:44 +0100 Subject: [PATCH 3/7] Refact UnpackedIntersection type --- test/types/populate.test.ts | 10 ++++++++-- types/index.d.ts | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 18ccab794bd..b9fe2906d48 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -176,7 +176,13 @@ function gh11496() { Users.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { expectAssignable(user?.friends[0]); - user?.friends[0].blocked; - user?.friends.map(friend => friend.blocked); + expectType(user?.friends[0].blocked); + expectType(user?.friends[0]._id); + + const firstFriendBlockedValue = user?.friends.map(friend => friend)[0]; + + // FIXME: These two test doesn't work, although it shows correct suggestions. + expectType(firstFriendBlockedValue?.blocked); + expectType(firstFriendBlockedValue?._id); }); } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index ef05ac31edc..cff4b5e455c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1812,8 +1812,13 @@ declare module 'mongoose' { type ReturnsNewDoc = { new: true } | { returnOriginal: false } | {returnDocument: 'after'}; type QueryWithHelpers = Query & THelpers; + // type UnpackedIntersection = T extends (infer V)[] ? (V & U)[] : T & U; - type UnpackedIntersection = T extends (infer A)[] ? (A & U)[] : keyof U extends never ? T & U: { [P in keyof U]: U[P] extends (infer B)[] ? (B & T)[] : U[P] & T }; + type UnpackedIntersection = T extends (infer A)[] + ? (A & U)[] + : keyof U extends never + ? T + : { [P in keyof U]: U[P] extends (infer B)[] ? (T & B)[] : T & U[P] }; type UnpackedIntersectionWithNull = T extends null ? UnpackedIntersection | null : UnpackedIntersection; From 6dffc06423ec85e6fba493cbe9717fb9595576ae Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 11:41:54 +0100 Subject: [PATCH 4/7] Remove commented line --- test/types/populate.test.ts | 4 ++-- types/index.d.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index b9fe2906d48..110e8a91a9e 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -151,7 +151,7 @@ function gh11321(): void { }); } -function gh11496() { +function gh11503() { interface Friend { blocked: boolean } @@ -181,7 +181,7 @@ function gh11496() { const firstFriendBlockedValue = user?.friends.map(friend => friend)[0]; - // FIXME: These two test doesn't work, although it shows correct suggestions. + // FIXME: These two tests doesn't work, although it shows correct suggestions. expectType(firstFriendBlockedValue?.blocked); expectType(firstFriendBlockedValue?._id); }); diff --git a/types/index.d.ts b/types/index.d.ts index cff4b5e455c..ce19e15bd02 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1812,7 +1812,6 @@ declare module 'mongoose' { type ReturnsNewDoc = { new: true } | { returnOriginal: false } | {returnDocument: 'after'}; type QueryWithHelpers = Query & THelpers; - // type UnpackedIntersection = T extends (infer V)[] ? (V & U)[] : T & U; type UnpackedIntersection = T extends (infer A)[] ? (A & U)[] From 69eb9bc8ea70cc672d78fe65d404a8dccc7d3226 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Thu, 10 Mar 2022 15:04:33 +0100 Subject: [PATCH 5/7] Refact UnpackedIntersection & delete UnpackedIntersectionWithNull --- test/types/populate.test.ts | 12 ++++-------- types/index.d.ts | 10 ++++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 110e8a91a9e..0d26f12cc57 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -167,7 +167,6 @@ function gh11503() { }); const Users = model('friends', UserSchema); - Users.findOne({}).populate('friends').then(user => { expectType(user?.friends[0]); expectError(user?.friends[0].blocked); @@ -175,14 +174,11 @@ function gh11503() { }); Users.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { - expectAssignable(user?.friends[0]); - expectType(user?.friends[0].blocked); - expectType(user?.friends[0]._id); - + expectAssignable(user?.friends[0]); + expectType(user?.friends[0].blocked); + expectType(user?.friends[0]._id); const firstFriendBlockedValue = user?.friends.map(friend => friend)[0]; - - // FIXME: These two tests doesn't work, although it shows correct suggestions. - expectType(firstFriendBlockedValue?.blocked); + expectType(firstFriendBlockedValue?.blocked); expectType(firstFriendBlockedValue?._id); }); } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index ce19e15bd02..18f974675a1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1814,12 +1814,10 @@ declare module 'mongoose' { type QueryWithHelpers = Query & THelpers; type UnpackedIntersection = T extends (infer A)[] - ? (A & U)[] + ? (Omit & U)[] : keyof U extends never ? T - : { [P in keyof U]: U[P] extends (infer B)[] ? (T & B)[] : T & U[P] }; - - type UnpackedIntersectionWithNull = T extends null ? UnpackedIntersection | null : UnpackedIntersection; + : Omit & { [P in keyof U]: U[P] extends (infer B)[] ? (HydratedDocument & B)[] : HydratedDocument & U[P]}; type ProjectionFields = {[Key in keyof Omit, '__v'>]?: any} & Record; @@ -2120,8 +2118,8 @@ declare module 'mongoose' { polygon(path: string, ...coordinatePairs: number[][]): this; /** Specifies paths which should be populated with other documents. */ - populate(path: string, select?: string | any, model?: string | Model, match?: any): QueryWithHelpers, DocType, THelpers, RawDocType>; - populate(options: PopulateOptions | Array): QueryWithHelpers, DocType, THelpers, RawDocType>; + populate(path: string, select?: string | any, model?: string | Model, match?: any): QueryWithHelpers, DocType, THelpers, RawDocType>; + populate(options: PopulateOptions | Array): QueryWithHelpers, DocType, THelpers, RawDocType>; /** Get/set the current projection (AKA fields). Pass `null` to remove the current projection. */ projection(): ProjectionFields | null; From 643a38d4925f1609c3442e603def28691c8b857a Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Fri, 11 Mar 2022 09:13:41 +0100 Subject: [PATCH 6/7] Refact UnpackedIntersection type & delete two wrong tests --- test/types/populate.test.ts | 2 -- types/index.d.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts index 0d26f12cc57..cdcb574461d 100644 --- a/test/types/populate.test.ts +++ b/test/types/populate.test.ts @@ -176,9 +176,7 @@ function gh11503() { Users.findOne({}).populate<{friends: Friend[]}>('friends').then(user => { expectAssignable(user?.friends[0]); expectType(user?.friends[0].blocked); - expectType(user?.friends[0]._id); const firstFriendBlockedValue = user?.friends.map(friend => friend)[0]; expectType(firstFriendBlockedValue?.blocked); - expectType(firstFriendBlockedValue?._id); }); } \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index 18f974675a1..3d454640e8e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1817,7 +1817,7 @@ declare module 'mongoose' { ? (Omit & U)[] : keyof U extends never ? T - : Omit & { [P in keyof U]: U[P] extends (infer B)[] ? (HydratedDocument & B)[] : HydratedDocument & U[P]}; + : Omit & U; type ProjectionFields = {[Key in keyof Omit, '__v'>]?: any} & Record; From 2c04ac9ccf8c6a36c66983672ec78f806472bc77 Mon Sep 17 00:00:00 2001 From: mohammad0-0ahmad Date: Fri, 11 Mar 2022 09:26:04 +0100 Subject: [PATCH 7/7] refact --- test/types/queries.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts index 5ff1ba2bacf..79e56648d6c 100644 --- a/test/types/queries.test.ts +++ b/test/types/queries.test.ts @@ -18,11 +18,11 @@ const schema: Schema, {}, QueryHelpers> = new endDate: Date }); -schema.query._byName = function(name: string): QueryWithHelpers { +schema.query._byName = function(name: string): QueryWithHelpers { return this.find({ name }); }; -schema.query.byName = function(name: string): QueryWithHelpers { +schema.query.byName = function(name: string): QueryWithHelpers { expectError(this.notAQueryHelper()); return this._byName(name); };