Skip to content

Commit abb823d

Browse files
authored
Merge pull request #14028 from ruxxzebre/fix-tinstancemethod-this
fix: "this" of Schema's instance methods
2 parents 463f063 + eb45389 commit abb823d

File tree

4 files changed

+146
-3
lines changed

4 files changed

+146
-3
lines changed

test/types/schema.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,3 +1230,124 @@ async function gh13797() {
12301230
expectType<IUser>(this); return '';
12311231
} } });
12321232
}
1233+
1234+
function gh14028_methods() {
1235+
// Methods that have access to `this` should have access to typing of other methods on the schema
1236+
interface IUser {
1237+
firstName: string;
1238+
lastName: string;
1239+
age: number;
1240+
}
1241+
interface IUserMethods {
1242+
fullName(): string;
1243+
isAdult(): boolean;
1244+
}
1245+
type UserModel = Model<IUser, {}, IUserMethods>;
1246+
1247+
// Define methods on schema
1248+
const schema = new Schema<IUser, UserModel, IUserMethods>({
1249+
firstName: { type: String, required: true },
1250+
lastName: { type: String, required: true },
1251+
age: { type: Number, required: true }
1252+
}, {
1253+
methods: {
1254+
fullName() {
1255+
// Expect type of `this` to have fullName method
1256+
expectType<IUserMethods['fullName']>(this.fullName);
1257+
return this.firstName + ' ' + this.lastName;
1258+
},
1259+
isAdult() {
1260+
// Expect type of `this` to have isAdult method
1261+
expectType<IUserMethods['isAdult']>(this.isAdult);
1262+
return this.age >= 18;
1263+
}
1264+
}
1265+
});
1266+
1267+
const User = model('User', schema);
1268+
const user = new User({ firstName: 'John', lastName: 'Doe', age: 20 });
1269+
// Trigger type assertions inside methods
1270+
user.fullName();
1271+
user.isAdult();
1272+
1273+
// Expect type of methods to be inferred if accessed directly
1274+
expectType<IUserMethods['fullName']>(schema.methods.fullName);
1275+
expectType<IUserMethods['isAdult']>(schema.methods.isAdult);
1276+
1277+
// Define methods outside of schema
1278+
const schema2 = new Schema<IUser, UserModel, IUserMethods>({
1279+
firstName: { type: String, required: true },
1280+
lastName: { type: String, required: true },
1281+
age: { type: Number, required: true }
1282+
});
1283+
1284+
schema2.methods.fullName = function fullName() {
1285+
expectType<IUserMethods['fullName']>(this.fullName);
1286+
return this.firstName + ' ' + this.lastName;
1287+
};
1288+
1289+
schema2.methods.isAdult = function isAdult() {
1290+
expectType<IUserMethods['isAdult']>(this.isAdult);
1291+
return true;
1292+
};
1293+
1294+
const User2 = model('User2', schema2);
1295+
const user2 = new User2({ firstName: 'John', lastName: 'Doe', age: 20 });
1296+
user2.fullName();
1297+
user2.isAdult();
1298+
1299+
type UserModelWithoutMethods = Model<IUser>;
1300+
// Skip InstanceMethods
1301+
const schema3 = new Schema<IUser, UserModelWithoutMethods>({
1302+
firstName: { type: String, required: true },
1303+
lastName: { type: String, required: true },
1304+
age: { type: Number, required: true }
1305+
}, {
1306+
methods: {
1307+
fullName() {
1308+
// Expect methods to still have access to `this` type
1309+
expectType<string>(this.firstName);
1310+
// As InstanceMethods type is not specified, expect type of this.fullName to be undefined
1311+
expectError<IUserMethods['fullName']>(this.fullName);
1312+
return this.firstName + ' ' + this.lastName;
1313+
}
1314+
}
1315+
});
1316+
1317+
const User3 = model('User2', schema3);
1318+
const user3 = new User3({ firstName: 'John', lastName: 'Doe', age: 20 });
1319+
expectError<string>(user3.fullName());
1320+
}
1321+
1322+
function gh14028_statics() {
1323+
// Methods that have access to `this` should have access to typing of other methods on the schema
1324+
interface IUser {
1325+
firstName: string;
1326+
lastName: string;
1327+
age: number;
1328+
}
1329+
interface IUserStatics {
1330+
createWithFullName(name: string): Promise<IUser>;
1331+
}
1332+
type UserModel = Model<IUser, {}>;
1333+
1334+
// Define statics on schema
1335+
const schema = new Schema<IUser, UserModel, {}, {}, {}, IUserStatics>({
1336+
firstName: { type: String, required: true },
1337+
lastName: { type: String, required: true },
1338+
age: { type: Number, required: true }
1339+
}, {
1340+
statics: {
1341+
createWithFullName(name: string) {
1342+
expectType<IUserStatics['createWithFullName']>(schema.statics.createWithFullName);
1343+
expectType<UserModel['create']>(this.create);
1344+
1345+
const [firstName, lastName] = name.split(' ');
1346+
return this.create({ firstName, lastName });
1347+
}
1348+
}
1349+
});
1350+
1351+
// Trigger type assertions inside statics
1352+
schema.statics.createWithFullName('John Doe');
1353+
}

types/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ declare module 'mongoose' {
297297
method(obj: Partial<TInstanceMethods>): this;
298298

299299
/** Object of currently defined methods on this schema. */
300-
methods: { [F in keyof TInstanceMethods]: TInstanceMethods[F] } & AnyObject;
300+
methods: AddThisParameter<TInstanceMethods, THydratedDocumentType> & AnyObject;
301301

302302
/** The original object passed to the schema constructor */
303303
obj: SchemaDefinition<SchemaDefinitionType<EnforcedDocType>, EnforcedDocType>;

types/schemaoptions.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ declare module 'mongoose' {
210210
TStaticMethods,
211211
{},
212212
{ [name: string]: (this: Model<DocType>, ...args: any[]) => unknown },
213-
{ [F in keyof TStaticMethods]: TStaticMethods[F] }
213+
AddThisParameter<TStaticMethods, Model<DocType>>
214214
>
215215

216216
/**
@@ -220,7 +220,7 @@ declare module 'mongoose' {
220220
TInstanceMethods,
221221
{},
222222
Record<any, (this: THydratedDocumentType, ...args: any) => unknown>,
223-
TInstanceMethods
223+
AddThisParameter<TInstanceMethods, THydratedDocumentType> & AnyObject
224224
>
225225

226226
/**

types/utility.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,26 @@ type IfEquals<T, U, Y = true, N = false> =
4040
(<G>() => G extends T ? 1 : 0) extends
4141
(<G>() => G extends U ? 1 : 0) ? Y : N;
4242

43+
/**
44+
* @summary Extracts 'this' parameter from a function, if it exists. Otherwise, returns fallback.
45+
* @param {T} T Function type to extract 'this' parameter from.
46+
* @param {F} F Fallback type to return if 'this' parameter does not exist.
47+
*/
48+
type ThisParameter<T, F> = T extends { (this: infer This): void }
49+
? This
50+
: F;
51+
52+
/**
53+
* @summary Decorates all functions in an object with 'this' parameter.
54+
* @param {T} T Object with functions as values to add 'D' parameter to as 'this'. {@link D}
55+
* @param {D} D The type to be added as 'this' parameter to all functions in {@link T}.
56+
*/
57+
type AddThisParameter<T, D> = {
58+
[K in keyof T]: T[K] extends (...args: infer A) => infer R
59+
? ThisParameter<T[K], unknown> extends unknown
60+
? (this: D, ...args: A) => R
61+
: T[K]
62+
: T[K];
63+
};
64+
4365
}

0 commit comments

Comments
 (0)