diff --git a/docs/guide.md b/docs/guide.md
index d78226415f5..ed64cc5a521 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -143,9 +143,18 @@ We may also define our own custom document instance methods.
```javascript
// define a schema
-const animalSchema = new Schema({ name: String, type: String });
+const animalSchema = new Schema({ name: String, type: String },
+{
+ // Assign a function to the "methods" object of our animalSchema through schema options.
+ // By following this approach, there is no need to create a separate TS type to define the type of the instance functions.
+ methods:{
+ findSimilarTypes(cb){
+ return mongoose.model('Animal').find({ type: this.type }, cb);
+ }
+ }
+});
-// assign a function to the "methods" object of our animalSchema
+// Or, assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
return mongoose.model('Animal').find({ type: this.type }, cb);
};
@@ -169,14 +178,28 @@ dog.findSimilarTypes((err, dogs) => {
-You can also add static functions to your model. There are two equivalent
+You can also add static functions to your model. There are three equivalent
ways to add a static:
+- Add a function property to the second argument of the schema-constructor (`statics`)
- Add a function property to `schema.statics`
- Call the [`Schema#static()` function](/docs/api.html#schema_Schema-static)
```javascript
-// Assign a function to the "statics" object of our animalSchema
+
+// define a schema
+const animalSchema = new Schema({ name: String, type: String },
+{
+ // Assign a function to the "statics" object of our animalSchema through schema options.
+ // By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
+ statics:{
+ findByName(name){
+ return this.find({ name: new RegExp(name, 'i') });
+ }
+ }
+});
+
+// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
return this.find({ name: new RegExp(name, 'i') });
};
@@ -197,6 +220,20 @@ but for mongoose queries. Query helper methods let you extend mongoose's
[chainable query builder API](./queries.html).
```javascript
+
+// define a schema
+const animalSchema = new Schema({ name: String, type: String },
+{
+ // Assign a function to the "query" object of our animalSchema through schema options.
+ // By following this approach, there is no need to create a separate TS type to define the type of the query functions.
+ query:{
+ byName(name){
+ return this.where({ name: new RegExp(name, 'i') })
+ }
+ }
+});
+
+// Or, Assign a function to the "query" object of our animalSchema
animalSchema.query.byName = function(name) {
return this.where({ name: new RegExp(name, 'i') })
};
@@ -409,6 +446,7 @@ Valid options:
- [read](#read)
- [writeConcern](#writeConcern)
- [shardKey](#shardKey)
+- [statics](#statics)
- [strict](#strict)
- [strictQuery](#strictQuery)
- [toJSON](#toJSON)
@@ -423,6 +461,8 @@ Valid options:
- [skipVersioning](#skipVersioning)
- [timestamps](#timestamps)
- [storeSubdocValidationError](#storeSubdocValidationError)
+- [methods](#methods)
+- [query](#query-helpers)
diff --git a/docs/typescript/query-helpers.md b/docs/typescript/query-helpers.md
index a9f99f6b32f..7c108d465f2 100644
--- a/docs/typescript/query-helpers.md
+++ b/docs/typescript/query-helpers.md
@@ -14,7 +14,10 @@ var Project = mongoose.model('Project', ProjectSchema);
Project.find().where('stars').gt(1000).byName('mongoose');
```
-In TypeScript, Mongoose's `Model` takes 3 generic parameters:
+In TypeScript, Mongoose does support manually typed and automatically typed Query Helpers.
+
+1- Manually typed:
+Mongoose's `Model` takes 3 generic parameters:
1. The `DocType`
2. a `TQueryHelpers` type
@@ -58,4 +61,32 @@ async function run(): Promise {
// Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
}
+```
+
+2- Automatically typed:
+Mongoose does support auto typed Query Helpers that it are supplied in schema options.
+Query Helpers functions can be defined as following:
+
+```typescript
+import { Schema, model } from 'mongoose';
+
+ const schema = new Schema({
+ name: { type: String, required: true },
+ stars: { type: Number, required: true }
+ }, {
+ query: {
+ byName(name) {
+ return this.find({ name: name });
+ }
+ }
+ });
+
+ const ProjectModel = model(
+ 'Project',
+ schema
+ );
+
+ // Equivalent to `ProjectModel.find({ stars: { $gt: 1000 }, name: 'mongoose' })`
+ await ProjectModel.find().where('stars').gt(1000).byName('mongoose');
+}
```
\ No newline at end of file
diff --git a/docs/typescript/schemas.md b/docs/typescript/schemas.md
index 79299b49834..001b3ab0f03 100644
--- a/docs/typescript/schemas.md
+++ b/docs/typescript/schemas.md
@@ -1,7 +1,11 @@
# Schemas in TypeScript
Mongoose [schemas](/docs/guide.html) are how you tell Mongoose what your documents look like.
-Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_.
+Mongoose schemas are separate from TypeScript interfaces, so you need to define both a _document interface_ and a _schema_ until V6.3.1.
+Mongoose supports auto typed schemas so you don't need to define additional typescript interface anymore but you are still able to do so.
+Mongoose provides a `InferSchemaType`, which infers the type of the auto typed schema document when needed.
+
+`Until mongoose V6.3.1:`
```typescript
import { Schema } from 'mongoose';
@@ -21,6 +25,37 @@ const schema = new Schema({
});
```
+`another approach:`
+
+```typescript
+import { Schema, InferSchemaType } from 'mongoose';
+
+// Document interface
+// No need to define TS interface any more.
+// interface User {
+// name: string;
+// email: string;
+// avatar?: string;
+// }
+
+// Schema
+const schema = new Schema({
+ name: { type: String, required: true },
+ email: { type: String, required: true },
+ avatar: String
+});
+
+type User = InferSchemaType;
+// InferSchemaType will determine the type as follows:
+// type User = {
+// name: string;
+// email: string;
+// avatar?: string;
+// }
+
+
+```
+
By default, Mongoose does **not** check if your document interface lines up with your schema.
For example, the above code won't throw an error if `email` is optional in the document interface, but `required` in `schema`.
@@ -51,7 +86,7 @@ Mongoose wraps `DocType` in a Mongoose document for cases like the `this` parame
For example:
```typescript
-schema.pre('save', function(): void {
+schema.pre('save', function (): void {
console.log(this.name); // TypeScript knows that `this` is a `mongoose.Document & User` by default
});
```
@@ -102,7 +137,7 @@ interface User {
const schema = new Schema>({
name: { type: String, required: true },
email: { type: String, required: true },
- avatar: String
+ avatar: String,
});
```
@@ -121,13 +156,13 @@ interface BlogPost {
}
interface User {
- tags: Types.Array,
- blogPosts: Types.DocumentArray
+ tags: Types.Array;
+ blogPosts: Types.DocumentArray;
}
const schema = new Schema>({
tags: [String],
- blogPosts: [{ title: String }]
+ blogPosts: [{ title: String }],
});
```
@@ -139,4 +174,4 @@ If you use `Types.DocumentArray` in the above case, you'll be able to `push()` a
const user = new User({ blogPosts: [] });
user.blogPosts.push({ title: 'test' }); // Would not work if you did `blogPosts: BlogPost[]`
-```
\ No newline at end of file
+```
diff --git a/docs/typescript/statics-and-methods.md b/docs/typescript/statics-and-methods.md
index 6897ccad225..b377538f006 100644
--- a/docs/typescript/statics-and-methods.md
+++ b/docs/typescript/statics-and-methods.md
@@ -65,6 +65,27 @@ const User = model('User', schema);
const answer: number = User.myStaticMethod(); // 42
```
+Mongoose does support auto typed static functions now that it is supplied in schema options.
+Statics functions can be defined as following:
+
+```typescript
+import { Schema, model } from 'mongoose';
+
+const schema = new Schema(
+ { name: String },
+ {
+ statics: {
+ myStaticMethod() {
+ return 42;
+ },
+ },
+ }
+);
+
+const User = model('User', schema);
+
+const answer = User.myStaticMethod(); // 42
+```
## Both Methods and Statics
Below is how you can define a model that has both methods and statics.
diff --git a/docs/typescript/statics.md b/docs/typescript/statics.md
new file mode 100644
index 00000000000..c8f163c8b37
--- /dev/null
+++ b/docs/typescript/statics.md
@@ -0,0 +1,47 @@
+# Statics in TypeScript
+
+Mongoose [models](/docs/models.html) do **not** have an explicit generic parameter for [statics](/docs/guide.html#statics).
+If your model has statics, we recommend creating an interface that [extends](https://www.typescriptlang.org/docs/handbook/interfaces.html) Mongoose's `Model` interface as shown below.
+
+```typescript
+import { Model, Schema, model } from 'mongoose';
+
+interface IUser {
+ name: string;
+}
+
+interface UserModel extends Model {
+ myStaticMethod(): number;
+}
+
+const schema = new Schema({ name: String });
+schema.static('myStaticMethod', function myStaticMethod() {
+ return 42;
+});
+
+const User = model('User', schema);
+
+const answer: number = User.myStaticMethod(); // 42
+```
+
+Mongoose does support auto typed static functions that it are supplied in schema options.
+Static functions can be defined by:
+
+```typescript
+import { Schema, model } from 'mongoose';
+
+const schema = new Schema(
+ { name: String },
+ {
+ statics: {
+ myStaticMethod() {
+ return 42;
+ },
+ },
+ }
+);
+
+const User = model('User', schema);
+
+const answer = User.myStaticMethod(); // 42
+```
diff --git a/lib/schema.js b/lib/schema.js
index 9de0f395e54..3ea4c3b5e32 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -107,11 +107,11 @@ function Schema(obj, options) {
this.inherits = {};
this.callQueue = [];
this._indexes = [];
- this.methods = {};
+ this.methods = (options && options.methods) || {};
this.methodOptions = {};
- this.statics = {};
+ this.statics = (options && options.statics) || {};
this.tree = {};
- this.query = {};
+ this.query = (options && options.query) || {};
this.childSchemas = [];
this.plugins = [];
// For internal debugging. Do not use this to try to save a schema in MDB.
diff --git a/test/document.test.js b/test/document.test.js
index 115d87a10d8..e1202951975 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -11455,3 +11455,12 @@ describe('document', function() {
assert.ok(baz.populated('bar'));
});
});
+
+describe('Check if instance function that is supplied in schema option is availabe', function() {
+ it('should give an instance function back rather than undefined', function ModelJS() {
+ const testSchema = new mongoose.Schema({}, { methods: { instanceFn() { return 'Returned from DocumentInstanceFn'; } } });
+ const TestModel = mongoose.model('TestModel', testSchema);
+ const TestDocument = new TestModel({});
+ assert.equal(TestDocument.instanceFn(), 'Returned from DocumentInstanceFn');
+ });
+});
\ No newline at end of file
diff --git a/test/model.test.js b/test/model.test.js
index 52a864058df..cf2f53c60ed 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -8505,6 +8505,13 @@ describe('Model', function() {
});
});
+describe('Check if static function that is supplied in schema option is available', function() {
+ it('should give a static function back rather than undefined', function ModelJS() {
+ const testSchema = new mongoose.Schema({}, { statics: { staticFn() { return 'Returned from staticFn'; } } });
+ const TestModel = mongoose.model('TestModel', testSchema);
+ assert.equal(TestModel.staticFn(), 'Returned from staticFn');
+ });
+});
diff --git a/test/query.test.js b/test/query.test.js
index d09a4135bdc..2f621c6cd38 100644
--- a/test/query.test.js
+++ b/test/query.test.js
@@ -3962,6 +3962,36 @@ describe('Query', function() {
assert.equal(result.length, 1);
assert.equal(result[0].name, '@foo.com');
});
+
+ it('should return query helper supplied in schema options query property instead of undefined', function(done) {
+ const Model = db.model('Test', new Schema({
+ userName: {
+ type: String,
+ required: [true, 'userName is required']
+ }
+ }, {
+ query: {
+ byUserName(userName) {
+ return this.where({ userName });
+ }
+ }
+ }));
+
+ Model.create({ userName: 'test' }, function(error) {
+ if (error instanceof Error) {
+ return done(error);
+ }
+ Model.find().byUserName('test').exec(function(error, docs) {
+ if (error instanceof Error) {
+ return done(error);
+ }
+ assert.equal(docs.length, 1);
+ assert.equal(docs[0].userName, 'test');
+ done();
+ });
+ });
+ });
+
it('allows a transform option for lean on a query gh-10423', async function() {
const arraySchema = new mongoose.Schema({
sub: String
diff --git a/test/types/create.test.ts b/test/types/create.test.ts
index 4492d113fca..debca167aeb 100644
--- a/test/types/create.test.ts
+++ b/test/types/create.test.ts
@@ -1,23 +1,185 @@
-import { Schema, model, Document, Types } from 'mongoose';
+import { Schema, model, Types, CallbackError } from 'mongoose';
+import { expectError, expectType } from 'tsd';
const schema: Schema = new Schema({ name: { type: 'String' } });
-interface ITest extends Document {
+interface ITest {
_id?: Types.ObjectId;
name?: string;
}
const Test = model('Test', schema);
-Test.create({ _id: '0'.repeat(24), name: 'test' }).then((doc: ITest) => console.log(doc.name));
+Test.create({ _id: '000000000000000000000000', name: 'test' }).then(doc => {
+ expectType(doc._id);
+ expectType(doc.name);
+ expectType(doc.isNew);
+});
-Test.create([{ name: 'test' }], { validateBeforeSave: false }).then((docs: ITest[]) => console.log(docs[0].name));
+Test.create({ _id: new Types.ObjectId('000000000000000000000000'), name: 'test' }).then((doc) => {
+ expectType(doc._id);
+ expectType(doc.name);
+ expectType(doc.isNew);
+});
-Test.create({ name: 'test' }, { name: 'test2' }).then((docs: ITest[]) => console.log(docs[0].name));
+Test.create([{ name: 'test' }], { validateBeforeSave: false }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
-Test.insertMany({ name: 'test' }).then((docs: ITest[]) => console.log(docs[0].name));
+Test.create({ name: 'test' }, { name: 'test2' }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[1]._id);
+ expectType(docs[1].name);
+});
-Test.create([{ name: 'test' }], { validateBeforeSave: true }).then((docs: ITest[]) => console.log(docs[0].name));
+Test.create([{ name: 'test' }], { validateBeforeSave: true }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+});
+
+
+Test.insertMany({ name: 'test' }, {}, (err, docs) => {
+ expectType(err);
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }, { lean: true }, (err, docs) => {
+ expectType(err);
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectError(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }, (err, docs) => {
+ expectType(err);
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }, {}, (err, docs) => {
+ expectType(err);
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany([{ name: 'test' }], { rawResult: true }, (err, result) => {
+ expectType(err);
+ expectType(result.acknowledged);
+ expectType(result.insertedCount);
+ expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds);
+});
+
+Test.insertMany([{ name: 'test' }], { rawResult: true }, (err, result) => {
+ expectType(err);
+ expectType(result.acknowledged);
+ expectType(result.insertedCount);
+ expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds);
+});
+
+Test.insertMany([{ name: 'test' }], { lean: true }, (err, docs) => {
+ expectType(err);
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectError(docs[0].isNew);
+});
+
+Test.insertMany([{ name: 'test' }], (err, docs) => {
+ expectType(err);
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ _id: '000000000000000000000000', name: 'test' }, (err, docs) => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000') }, (err, docs) => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }, {}).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }, { lean: true }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectError(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ name: 'test' }, {}).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany([{ name: 'test' }], { rawResult: true }).then(result => {
+ expectType(result.acknowledged);
+ expectType(result.insertedCount);
+ expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds);
+});
+
+Test.insertMany([{ name: 'test' }], { rawResult: true }).then(result => {
+ expectType(result.acknowledged);
+ expectType(result.insertedCount);
+ expectType<{ [key: number]: Types.ObjectId; }>(result.insertedIds);
+});
+
+Test.insertMany([{ name: 'test' }], { lean: true }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectError(docs[0].isNew);
+});
+
+Test.insertMany([{ name: 'test' }], { lean: false }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany([{ name: 'test' }], { }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany([{ name: 'test' }]).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ _id: '000000000000000000000000', name: 'test' }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
+
+Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000'), name: 'test' }).then(docs => {
+ expectType(docs[0]._id);
+ expectType(docs[0].name);
+ expectType(docs[0].isNew);
+});
(async() => {
const [t1] = await Test.create([{ name: 'test' }]);
diff --git a/test/types/document.test.ts b/test/types/document.test.ts
index 3f8d8ce2176..3319a4a59a6 100644
--- a/test/types/document.test.ts
+++ b/test/types/document.test.ts
@@ -1,5 +1,7 @@
import { Schema, model, Model, Document, Types } from 'mongoose';
-import { expectError, expectType } from 'tsd';
+import { expectAssignable, expectError, expectType } from 'tsd';
+import { autoTypedModel } from './models.test';
+import { AutoTypedSchemaType } from './schema.test';
const Drink = model('Drink', new Schema({
name: String
@@ -52,15 +54,16 @@ void async function run() {
test.validateSync({ pathsToSkip: ['name', 'age'] });
test.validateSync({ pathsToSkip: 'name age' });
test.validateSync({ pathsToSkip: 'name age', blub: 1 });
- expectType>(test.save());
- expectType>(test.save({}));
+ const x = test.save();
+ expectAssignable>(test.save());
+ expectAssignable>(test.save({}));
expectType(test.save({}, (err, doc) => {
expectType(err);
- expectType(doc);
+ expectAssignable(doc);
}));
expectType(test.save((err, doc) => {
expectType(err);
- expectType(doc);
+ expectAssignable(doc);
}));
})();
@@ -180,4 +183,19 @@ function gh11435() {
async function gh11598() {
const doc = await Test.findOne().orFail();
doc.populate('favoritDrink', undefined, model('temp', new Schema()));
+}
+
+function autoTypedDocument() {
+ const AutoTypedModel = autoTypedModel();
+ const AutoTypeModelInstance = new AutoTypedModel({ unExistProperty: 1, description: 2 });
+
+ expectType(AutoTypeModelInstance.userName);
+ expectType(AutoTypeModelInstance.favoritDrink);
+ expectType(AutoTypeModelInstance.favoritColorMode);
+ expectType(AutoTypeModelInstance.unExistProperty);
+ expectType(AutoTypeModelInstance.description);
+
+ // Document-Methods-tests
+ expectType>(new AutoTypedModel().instanceFn());
+
}
\ No newline at end of file
diff --git a/test/types/models.test.ts b/test/types/models.test.ts
index 1c5c75a6701..29b1fe4ebb0 100644
--- a/test/types/models.test.ts
+++ b/test/types/models.test.ts
@@ -1,5 +1,7 @@
-import { Schema, Document, Model, Types, connection, model } from 'mongoose';
-import { expectError } from 'tsd';
+import { ObjectId } from 'bson';
+import { Schema, Document, Model, connection, model, Types } from 'mongoose';
+import { expectError, expectType } from 'tsd';
+import { AutoTypedSchemaType, autoTypedSchema } from './schema.test';
function conventionalSyntax(): void {
interface ITest extends Document {
@@ -40,9 +42,7 @@ function rawDocSyntax(): void {
const Test = connection.model('Test', TestSchema);
- const bar = (SomeModel: Model) => console.log(SomeModel);
-
- bar(Test);
+ expectType>(Test);
const doc = new Test({ foo: '42' });
console.log(doc.foo);
@@ -83,7 +83,7 @@ async function insertManyTest() {
});
const res = await Test.insertMany([{ foo: 'bar' }], { rawResult: true });
- const ids: Types.ObjectId[] = Object.values(res.insertedIds);
+ expectType(res.insertedIds[0]);
}
function schemaStaticsWithoutGenerics() {
@@ -139,13 +139,17 @@ async function gh10359() {
lastName: string;
}
- async function foo(model: Model): Promise {
- const doc: T | null = await model.findOne({ groupId: 'test' }).lean().exec();
+ async function foo(model: Model) {
+ const doc = await model.findOne({ groupId: 'test' }).lean().exec();
+ expectType(doc?.firstName);
+ expectType(doc?.lastName);
+ expectType(doc?._id);
+ expectType(doc?.groupId);
return doc;
}
const UserModel = model('gh10359', new Schema({ firstName: String, lastName: String, groupId: String }));
- const u: User | null = await foo(UserModel);
+ foo(UserModel);
}
const ExpiresSchema = new Schema({
@@ -234,4 +238,34 @@ function bulkWrite() {
}
];
M.bulkWrite(ops);
+}
+
+export function autoTypedModel() {
+ const AutoTypedSchema = autoTypedSchema();
+ const AutoTypedModel = model('AutoTypeModel', AutoTypedSchema);
+
+ (async() => {
+ // Model-functions-test
+ // Create should works with arbitrary objects.
+ const randomObject = await AutoTypedModel.create({ unExistKey: 'unExistKey', description: 'st' });
+ expectType(randomObject.unExistKey);
+ expectType(randomObject.userName);
+
+ const testDoc1 = await AutoTypedModel.create({ userName: 'M0_0a' });
+ expectType(testDoc1.userName);
+ expectType(testDoc1.description);
+
+ const testDoc2 = await AutoTypedModel.insertMany([{ userName: 'M0_0a' }]);
+ expectType(testDoc2[0].userName);
+ expectType(testDoc2[0]?.description);
+
+ const testDoc3 = await AutoTypedModel.findOne({ userName: 'M0_0a' });
+ expectType(testDoc3?.userName);
+ expectType(testDoc3?.description);
+
+ // Model-statics-functions-test
+ expectType>(AutoTypedModel.staticFn());
+
+ })();
+ return AutoTypedModel;
}
\ No newline at end of file
diff --git a/test/types/populate.test.ts b/test/types/populate.test.ts
index 46e2021946e..a3495d2f4c0 100644
--- a/test/types/populate.test.ts
+++ b/test/types/populate.test.ts
@@ -72,6 +72,7 @@ const Story = model('Story', storySchema);
await story.populate('author');
await story.populate({ path: 'fans' });
+ await story.populate({ path: 'fans', model: Person });
await story.populate(['author']);
await story.populate([{ path: 'fans' }]);
await story.populate(['author', { path: 'fans' }]);
diff --git a/test/types/queries.test.ts b/test/types/queries.test.ts
index 1f029a99158..5d5ad58948f 100644
--- a/test/types/queries.test.ts
+++ b/test/types/queries.test.ts
@@ -1,6 +1,8 @@
import { HydratedDocument, Schema, model, Document, Types, Query, Model, QueryWithHelpers, PopulatedDoc, FilterQuery, UpdateQuery } from 'mongoose';
import { ObjectId } from 'mongodb';
import { expectError, expectType } from 'tsd';
+import { autoTypedModel } from './models.test';
+import { AutoTypedSchemaType } from './schema.test';
interface QueryHelpers {
_byName(this: QueryWithHelpers, name: string): QueryWithHelpers, ITest, QueryHelpers>;
@@ -161,10 +163,10 @@ function testGenericQuery(): void {
function eachAsync(): void {
Test.find().cursor().eachAsync((doc) => {
- expectType<(ITest & { _id: any; })>(doc);
+ expectType<(ITest & { _id: Types.ObjectId; })>(doc);
});
Test.find().cursor().eachAsync((docs) => {
- expectType<(ITest & { _id: any; })[]>(docs);
+ expectType<(ITest & { _id: Types.ObjectId; })[]>(docs);
}, { batchSize: 2 });
}
@@ -286,4 +288,10 @@ async function gh11602(): Promise {
expectError(updateResult.lastErrorObject?.modifiedCount);
expectType(updateResult.lastErrorObject?.updatedExisting);
expectType(updateResult.lastErrorObject?.upserted);
-}
\ No newline at end of file
+}
+
+function autoTypedQuery() {
+ const AutoTypedModel = autoTypedModel();
+ const query = AutoTypedModel.find();
+ expectType(AutoTypedModel.find().byUserName(''));
+}
diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts
index bcc13d2484b..a510596d045 100644
--- a/test/types/schema.test.ts
+++ b/test/types/schema.test.ts
@@ -1,5 +1,5 @@
-import { Schema, Document, SchemaDefinition, Model, Types } from 'mongoose';
-import { expectType, expectNotType, expectError } from 'tsd';
+import { Schema, Document, SchemaDefinition, Model, Types, InferSchemaType, SchemaType, Query, HydratedDocument } from 'mongoose';
+import { expectType, expectError, expectAssignable } from 'tsd';
enum Genre {
Action,
@@ -318,4 +318,197 @@ function gh10900(): void {
const patientSchema = new Schema({
menuStatus: { type: Schema.Types.Mixed, default: {} }
});
-}
\ No newline at end of file
+}
+
+export function autoTypedSchema() {
+ // Test auto schema type obtaining with all possible path types.
+
+ class Int8 extends SchemaType {
+ constructor(key, options) {
+ super(key, options, 'Int8');
+ }
+ cast(val) {
+ let _val = Number(val);
+ if (isNaN(_val)) {
+ throw new Error('Int8: ' + val + ' is not a number');
+ }
+ _val = Math.round(_val);
+ if (_val < -0x80 || _val > 0x7F) {
+ throw new Error('Int8: ' + val +
+ ' is outside of the range of valid 8-bit ints');
+ }
+ return _val;
+ }
+ }
+
+ type TestSchemaType = {
+ string1?: string;
+ string2?: string;
+ string3?: string;
+ string4?: string;
+ number1?: number;
+ number2?: number;
+ number3?: number;
+ number4?: number;
+ date1?: Date;
+ date2?: Date;
+ date3?: Date;
+ date4?: Date;
+ buffer1?: Buffer;
+ buffer2?: Buffer;
+ buffer3?: Buffer;
+ buffer4?: Buffer;
+ boolean1?: boolean;
+ boolean2?: boolean;
+ boolean3?: boolean;
+ boolean4?: boolean;
+ mixed1?: any;
+ mixed2?: any;
+ mixed3?: any;
+ objectId1?: Schema.Types.ObjectId;
+ objectId2?: Schema.Types.ObjectId;
+ objectId3?: Schema.Types.ObjectId;
+ customSchema?: Int8;
+ map1?: Map;
+ map2?: Map;
+ array1?: string[];
+ array2?: any[];
+ array3?: any[];
+ array4?: any[];
+ array5?: any[];
+ decimal1?: Schema.Types.Decimal128;
+ decimal2?: Schema.Types.Decimal128;
+ decimal3?: Schema.Types.Decimal128;
+ };
+
+ const TestSchema = new Schema({
+ string1: String,
+ string2: 'String',
+ string3: 'string',
+ string4: Schema.Types.String,
+ number1: Number,
+ number2: 'Number',
+ number3: 'number',
+ number4: Schema.Types.Number,
+ date1: Date,
+ date2: 'Date',
+ date3: 'date',
+ date4: Schema.Types.Date,
+ buffer1: Buffer,
+ buffer2: 'Buffer',
+ buffer3: 'buffer',
+ buffer4: Schema.Types.Buffer,
+ boolean1: Boolean,
+ boolean2: 'Boolean',
+ boolean3: 'boolean',
+ boolean4: Schema.Types.Boolean,
+ mixed1: Object,
+ mixed2: {},
+ mixed3: Schema.Types.Mixed,
+ objectId1: Schema.Types.ObjectId,
+ objectId2: 'ObjectId',
+ objectId3: 'objectId',
+ customSchema: Int8,
+ map1: { type: Map, of: String },
+ map2: { type: Map, of: Number },
+ array1: [String],
+ array2: Array,
+ array3: [Schema.Types.Mixed],
+ array4: [{}],
+ array5: [],
+ decimal1: Schema.Types.Decimal128,
+ decimal2: 'Decimal128',
+ decimal3: 'decimal128'
+ });
+
+ type InferredTestSchemaType = InferSchemaType;
+
+ expectType({} as InferredTestSchemaType);
+
+ const SchemaWithCustomTypeKey = new Schema({
+ name: {
+ customTypeKey: String,
+ required: true
+ }
+ }, {
+ typeKey: 'customTypeKey'
+ });
+
+ expectType({} as InferSchemaType['name']);
+
+ const AutoTypedSchema = new Schema({
+ userName: {
+ type: String,
+ required: [true, 'userName is required']
+ },
+ description: String,
+ nested: new Schema({
+ age: {
+ type: Number,
+ required: true
+ },
+ hobby: {
+ type: String,
+ required: false
+ }
+ }),
+ favoritDrink: {
+ type: String,
+ enum: ['Coffee', 'Tea']
+ },
+ favoritColorMode: {
+ type: String,
+ enum: {
+ values: ['dark', 'light'],
+ message: '{VALUE} is not supported'
+ },
+ required: true
+ }
+ }, {
+ statics: {
+ staticFn() {
+ expectType>(this);
+ return 'Returned from staticFn' as const;
+ }
+ },
+ methods: {
+ instanceFn() {
+ expectType>(this);
+ return 'Returned from DocumentInstanceFn' as const;
+ }
+ },
+ query: {
+ byUserName(userName) {
+ expectAssignable>(this);
+ return this.where({ userName });
+ }
+ }
+ });
+
+ type InferredSchemaType = InferSchemaType;
+
+ expectType({} as InferredSchemaType);
+
+ expectError({} as InferredSchemaType);
+
+ return AutoTypedSchema;
+}
+
+export type AutoTypedSchemaType = {
+ schema: {
+ userName: string;
+ description?: string;
+ nested?: {
+ age: number;
+ hobby?: string
+ },
+ favoritDrink?: 'Tea' | 'Coffee',
+ favoritColorMode: 'dark' | 'light'
+ }
+ , statics: {
+ staticFn: () => 'Returned from staticFn'
+ },
+ methods: {
+ instanceFn: () => 'Returned from DocumentInstanceFn'
+ },
+};
\ No newline at end of file
diff --git a/test/types/utility.test.ts b/test/types/utility.test.ts
new file mode 100644
index 00000000000..03dc3d7e9af
--- /dev/null
+++ b/test/types/utility.test.ts
@@ -0,0 +1,13 @@
+import { MergeType } from 'mongoose';
+import { expectType } from 'tsd';
+
+type A = { a: string, c: number };
+type B = { a: number, b: string };
+
+expectType({} as MergeType['a']);
+expectType({} as MergeType['b']);
+expectType({} as MergeType['c']);
+
+expectType({} as MergeType['a']);
+expectType({} as MergeType['b']);
+expectType({} as MergeType['c']);
diff --git a/tsconfig.json b/tsconfig.json
index 98bd39acf40..10f087f4bf2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "strict": true,
"strictNullChecks": true,
"paths": {
"mongoose" : ["./types/index.d.ts"]
diff --git a/types/document.d.ts b/types/document.d.ts
index 157ef53a6ab..2316e3e5d22 100644
--- a/types/document.d.ts
+++ b/types/document.d.ts
@@ -196,8 +196,8 @@ declare module 'mongoose' {
/** Populates document references. */
populate(path: string | PopulateOptions | (string | PopulateOptions)[]): Promise;
populate(path: string | PopulateOptions | (string | PopulateOptions)[], callback: Callback): void;
- populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions): Promise;
- populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions, callback?: Callback): void;
+ populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions): Promise;
+ populate(path: string, select?: string | AnyObject, model?: Model, match?: AnyObject, options?: PopulateOptions, callback?: Callback): void;
/** Gets _id(s) used during population of the given `path`. If the path was not populated, returns `undefined`. */
populated(path: string): any;
diff --git a/types/index.d.ts b/types/index.d.ts
index 3695eadcd9b..46e4cbc22f5 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -19,6 +19,7 @@
///
///
///
+///
declare class NativeDate extends global.Date { }
@@ -61,10 +62,18 @@ declare module 'mongoose' {
/* ! ignore */
export type CompileModelOptions = { overwriteModels?: boolean, connection?: Connection };
+ export function model(
+ name: string,
+ schema?: TSchema,
+ collection?: string,
+ options?: CompileModelOptions
+ ): Model, ObtainSchemaGeneric, ObtainSchemaGeneric, {}, TSchema> & ObtainSchemaGeneric;
+
export function model(name: string, schema?: Schema | Schema, collection?: string, options?: CompileModelOptions): Model;
+
export function model(
name: string,
- schema?: Schema,
+ schema?: Schema,
collection?: string,
options?: CompileModelOptions
): U;
@@ -91,7 +100,16 @@ declare module 'mongoose' {
export interface AnyObject {
[k: string]: any
}
- export type Require_id = T extends { _id?: any } ? (T & { _id: T['_id'] }) : (T & { _id: Types.ObjectId });
+
+ export type Require_id = T extends { _id?: infer U }
+ ? U extends any
+ ? (T & { _id: Types.ObjectId })
+ : T & Required<{ _id: U }>
+ : T & { _id: Types.ObjectId };
+
+ export type RequireOnlyTypedId = T extends { _id?: infer U; }
+ ? Required<{ _id: U }>
+ : { _id: Types.ObjectId };
export type HydratedDocument = DocType extends Document ? Require_id : (Document & Require_id & TVirtuals & TMethodsAndOverrides);
@@ -120,14 +138,18 @@ declare module 'mongoose' {
useProjection?: boolean;
}
- export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any> extends events.EventEmitter {
+ export class Schema, TInstanceMethods = {}, TQueryHelpers = {}, TVirtuals = any,
+ TStaticMethods = {},
+ TPathTypeKey extends TypeKeyBaseType = DefaultTypeKey,
+ DocType extends ObtainDocumentType = ObtainDocumentType>
+ extends events.EventEmitter {
/**
* Create a new schema
*/
- constructor(definition?: SchemaDefinition>, options?: SchemaOptions);
+ constructor(definition?: SchemaDefinition> | DocType, options?: SchemaOptions);
/** Adds key path / schema type pairs to this schema. */
- add(obj: SchemaDefinition> | Schema, prefix?: string): this;
+ add(obj: SchemaDefinition> | Schema, prefix?: string): this;
/**
* Array of child schemas (from document arrays and single nested subdocs)
@@ -180,7 +202,7 @@ declare module 'mongoose' {
methods: { [F in keyof TInstanceMethods]: TInstanceMethods[F] } & AnyObject;
/** The original object passed to the schema constructor */
- obj: SchemaDefinition>;
+ obj: SchemaDefinition>;
/** Gets/sets schema paths. */
path(path: string): ResultType;
diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts
new file mode 100644
index 00000000000..cc8c89e5cab
--- /dev/null
+++ b/types/inferschematype.d.ts
@@ -0,0 +1,155 @@
+import { Schema, InferSchemaType, SchemaType, SchemaTypeOptions, TypeKeyBaseType } from 'mongoose';
+
+declare module 'mongoose' {
+ /**
+ * @summary Obtains document schema type.
+ * @description Obtains document schema type from document Definition OR returns enforced schema type if it's provided.
+ * @param {DocDefinition} DocDefinition A generic equals to the type of document definition "provided in as first parameter in Schema constructor".
+ * @param {EnforcedDocType} EnforcedDocType A generic type enforced by user "provided before schema constructor".
+ * @param {TypeKey} TypeKey A generic of literal string type.
+ */
+ type ObtainDocumentType =
+ IsItRecordAndNotAny extends true ? EnforcedDocType : {
+ [K in keyof (RequiredPaths &
+ OptionalPaths)]: ObtainDocumentPathType;
+ };
+
+ /**
+ * @summary Obtains document schema type from Schema instance.
+ * @param {SchemaType} SchemaType A generic of schema type instance.
+ * @example
+ * const userSchema = new Schema({userName:String});
+ * type UserType = InferSchemaType;
+ * // result
+ * type UserType = {userName?: string}
+ */
+ type InferSchemaType = ObtainSchemaGeneric ;
+
+ /**
+ * @summary Obtains schema Generic type by using generic alias.
+ * @param {TSchema} TSchema A generic of schema type instance.
+ * @param {alias} alias Targeted generic alias.
+ */
+ type ObtainSchemaGeneric =
+ TSchema extends Schema
+ ? {
+ EnforcedDocType: EnforcedDocType;
+ M: M;
+ TInstanceMethods: TInstanceMethods;
+ TQueryHelpers: TQueryHelpers;
+ TVirtuals: TVirtuals;
+ TStaticMethods: TStaticMethods;
+ TPathTypeKey: TPathTypeKey;
+ DocType: DocType;
+ }[alias]
+ : unknown;
+}
+/**
+ * @summary Checks if a type is "Record" or "any".
+ * @description It Helps to check if user has provided schema type "EnforcedDocType"
+ * @param {T} T A generic type to be checked.
+ * @returns true if {@link T} is Record OR false if {@link T} is of any type.
+ */
+type IsItRecordAndNotAny = IfEquals ? true : false>;
+
+/**
+ * @summary Checks if two types are identical.
+ * @param {T} T The first type to be compared with {@link U}.
+ * @param {U} U The seconde type to be compared with {@link T}.
+ * @param {Y} Y A type to be returned if {@link T} & {@link U} are identical.
+ * @param {N} N A type to be returned if {@link T} & {@link U} are not identical.
+ */
+type IfEquals =
+ (() => G extends T ? 1 : 0) extends
+ (() => G extends U ? 1 : 0) ? Y : N;
+
+/**
+ * @summary Required path base type.
+ * @description It helps to check whereas if a path is required OR optional.
+ */
+type RequiredPathBaseType = { required: true | [true, string | undefined] };
+
+/**
+ * @summary Path base type defined by using TypeKey
+ * @description It helps to check if a path is defined by TypeKey OR not.
+ * @param {TypeKey} TypeKey A literal string refers to path type property key.
+ */
+type PathWithTypePropertyBaseType = { [k in TypeKey]: any };
+
+/**
+ * @summary A Utility to obtain schema's required path keys.
+ * @param {T} T A generic refers to document definition.
+ * @returns required paths keys of document definition.
+ */
+type RequiredPathKeys = {
+ [K in keyof T]: T[K] extends RequiredPathBaseType ? IfEquals : never;
+}[keyof T];
+
+/**
+ * @summary A Utility to obtain schema's required paths.
+ * @param {T} T A generic refers to document definition.
+ * @returns a record contains required paths with the corresponding type.
+ */
+type RequiredPaths = {
+ [K in RequiredPathKeys]: T[K];
+};
+
+/**
+ * @summary A Utility to obtain schema's optional path keys.
+ * @param {T} T A generic refers to document definition.
+ * @returns optional paths keys of document definition.
+ */
+type OptionalPathKeys = {
+ [K in keyof T]: T[K] extends RequiredPathBaseType ? never : K;
+}[keyof T];
+
+/**
+ * @summary A Utility to obtain schema's optional paths.
+ * @param {T} T A generic refers to document definition.
+ * @returns a record contains optional paths with the corresponding type.
+ */
+type OptionalPaths = {
+ [K in OptionalPathKeys]?: T[K];
+};
+
+/**
+ * @summary Obtains schema Path type.
+ * @description Obtains Path type by calling {@link ResolvePathType} OR by calling {@link InferSchemaType} if path of schema type.
+ * @param {PathValueType} PathValueType Document definition path type.
+ * @param {TypeKey} TypeKey A generic refers to document definition.
+ */
+type ObtainDocumentPathType = PathValueType extends Schema
+ ? InferSchemaType
+ : ResolvePathType<
+ PathValueType extends PathWithTypePropertyBaseType ? PathValueType[TypeKey] : PathValueType,
+ PathValueType extends PathWithTypePropertyBaseType ? Omit : {}
+ >;
+
+/**
+ * @param {T} T A generic refers to string path enums.
+ * @returns Path enum values type as literal strings or string.
+ */
+type PathEnumOrString['enum']> = T extends (infer E)[] ? E : T extends { values: any } ? PathEnumOrString : string;
+
+/**
+ * @summary Resolve path type by returning the corresponding type.
+ * @param {PathValueType} PathValueType Document definition path type.
+ * @param {Options} Options Document definition path options except path type.
+ * @returns Number, "Number" or "number" will be resolved to string type.
+ */
+type ResolvePathType = {}> =
+ PathValueType extends (infer Item)[] ? IfEquals- >[] :
+ PathValueType extends StringConstructor | 'string' | 'String' | typeof Schema.Types.String ? PathEnumOrString :
+ PathValueType extends NumberConstructor | 'number' | 'Number' | typeof Schema.Types.Number ? number :
+ PathValueType extends DateConstructor | 'date' | 'Date' | typeof Schema.Types.Date ? Date :
+ PathValueType extends BufferConstructor | 'buffer' | 'Buffer' | typeof Schema.Types.Buffer ? Buffer :
+ PathValueType extends BooleanConstructor | 'boolean' | 'Boolean' | typeof Schema.Types.Boolean ? boolean :
+ PathValueType extends 'objectId' | 'ObjectId' | typeof Schema.Types.ObjectId ? Schema.Types.ObjectId :
+ PathValueType extends 'decimal128' | 'Decimal128' | typeof Schema.Types.Decimal128 ? Schema.Types.Decimal128 :
+ PathValueType extends MapConstructor ? Map> :
+ PathValueType extends ArrayConstructor ? any[] :
+ PathValueType extends typeof Schema.Types.Mixed ? any:
+ IfEquals extends true ? any:
+ IfEquals extends true ? any:
+ PathValueType extends typeof SchemaType ? PathValueType['prototype'] :
+ unknown;
\ No newline at end of file
diff --git a/types/models.d.ts b/types/models.d.ts
index 3de92a2823f..d15675dbb1b 100644
--- a/types/models.d.ts
+++ b/types/models.d.ts
@@ -114,12 +114,12 @@ declare module 'mongoose' {
}
const Model: Model;
- interface Model