-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
[TypeScript] Usage of 'any' in HydratedDocument and Document types causes document's _id
property to have 'any' type
#11085
Comments
Another issue I think I found in the course of testing this stuff is that it looks like any types that extend Document aren't This may be intended, as I know the Mongoose docs advise against extending Example: interface IBook extends Document {
_id: Types.ObjectId;
author: string;
title: string;
}
interface BookInstanceMethods {
titleAndAuthor: (this: IBook) => string;
}
type BookModel = Model<IBook, Record<string, unknown>, BookInstanceMethods>;
const bookSchema = new Schema<IBook, BookModel, BookInstanceMethods>({
author: String,
title: String
});
bookSchema.method('titleAndAuthor', function (this: IBook) {
return `'${this.title}' by ${this.author}`;
});
const Book = model('Book', bookSchema);
const book = new Book();
book.titleAndAuthor(); // titleAndAuthor is undefined
book._id; // _id is Types.ObjectId In this example, "_id" is correctly recognized as an Behind the scenes what appears to be happening is: When If the example is changed only by removing interface IBook {
_id: Types.ObjectId;
author: string;
title: string;
} TypeScript now correctly recognizes the If I modify the definition of Before: export type HydratedDocument<
DocType,
TMethods = {},
TVirtuals = {}
> = DocType extends Document
? Require_id<DocType>
: (Document<any, any, DocType> & Require_id<DocType> & TVirtuals & TMethods); After: export type HydratedDocument<
DocType,
TMethods = {},
TVirtuals = {}
> = (DocType extends Document
? Require_id<DocType>
: Document<any, any, DocType> & Require_id<DocType>) &
TVirtuals &
TMethods; It could also be simplified a bit to not repeat export type HydratedDocument<
DocType,
TMethods = {},
TVirtuals = {}
> = Require_id<DocType> &
(DocType extends Document ? {} : Document<any, any, DocType>) &
TVirtuals &
TMethods; If this actually is unintended behavior it might deserve its own issue, which I'd be happy to submit. |
newUser._id; // _id: any
newUserWithId._id; // _id: any
newUserWithStringId._id; // _id: any
newUserDocument._id; // _id: any
newUserWithIdDocument._id; // _id: Types.ObjectId
newUserWithStringIdDocument._id; // _id: any for me is instead newUser._id; // _id: Types.ObjectId
newUserWithId._id; // _id: Types.ObjectId
newUserWithStringId._id; // _id: string
newUserDocument._id; // _id: any
newUserWithIdDocument._id; // _id: Types.ObjectId
newUserWithStringIdDocument._id; // _id: string |
Hm, well that's embarrassing. Were you using the same versions of everything that I was, or if not could you share your versions so I can test with those? Edit: I'm still getting the same results as before. Maybe I should've been more specific instead of assuming it'd be implied, but I'm using the VS Code (version 1.64.0-insider) intellisense popups to determine these type results, and the actual compile errors are being produced by both the VS Code ESLint extension and when I try to execute the code using |
Also, I'm curious if you get the same result from the first, smaller example I gave: import { Types } from 'mongoose';
interface Type1 {
_id?: any;
}
interface Type2 {
_id: Types.ObjectId;
}
type IntersectionType = Type1 & Type2;
const intersectionVar: IntersectionType = { _id: new Types.ObjectId() };
intersectionVar._id; // _id: any If |
Nevermind - this is talking about unions while we're dealing with intersections, so may not be relevant. I've been reversing the two in my mind. Both unions and intersections seem to have the same behavior in this case, though. |
Sorry to keep making further comments/edits, I'm just trying to figure this out as it's affecting the project I'm working on. Here's a repro on the TypeScript playground showing the same Intellisense results I'm seeing in my VS Code environment. It's just using their default tsconfig settings on there. |
If you add interface UserWithId extends Document {
_id: Types.ObjectId;
username: string;
email: string;
}
const userWithIdSchema = new Schema<UserWithId>({
username: String,
email: String,
});
const UserWithIdModel = model('UserWithId', userWithIdSchema);
const newUserWithId = new UserWithIdModel();
newUserWithId._id; // Check type of _id Docs does not recommend extending Document though, but I can't get why. On my projects I always had extended. |
@DavideViolante Yeah, exactly. I only encountered the issue because I was trying to do as the Mongoose documentation suggests and avoid extending Currently the only way I can get |
@drewkht On the smaller example it resolves to _id: any as oppossed to types.objectId like it should. |
Re: OP's first comment, we'll apply the Re: OP's 2nd comment, with the import { model, Schema, Types, Document, Model } from 'mongoose';
interface IBook {
_id: Types.ObjectId;
author: string;
title: string;
}
interface BookInstanceMethods {
titleAndAuthor: (this: IBook) => string;
}
type BookModel = Model<IBook, Record<string, unknown>, BookInstanceMethods>;
const bookSchema = new Schema<IBook, BookModel, BookInstanceMethods>({
author: String,
title: String
});
bookSchema.method('titleAndAuthor', function (this: IBook) {
return `'${this.title}' by ${this.author}`;
});
const Book = model<IBook, BookModel>('Book', bookSchema); // <-- note the generics here
const book = new Book();
book.titleAndAuthor(); // titleAndAuthor is undefined
book._id; // _id is Types.ObjectId
// "Type 'ObjectId' is not assignable to type 'number'." as expected
const _id: number = book._id; It is a bit tricky to test whether |
Thanks, just for the records, the example above works now: import { Types, Schema, model, Model } from 'mongoose'
interface UserWithId {
_id: Types.ObjectId;
username: string;
email: string;
}
type UserModel = Model<UserWithId, Record<string, unknown>>;
const userWithIdSchema = new Schema<UserWithId, UserModel>({
username: String,
email: String,
});
const UserWithIdModel = model<UserWithId, UserModel>('UserWithId', userWithIdSchema);
const newUserWithId = new UserWithIdModel();
newUserWithId._id; // _id is Types.ObjectId (even without using UserModel type) |
Do you want to request a feature or report a bug?
Bug
What is the current behavior?
After defining an interface
T
, creating a schemaSchema<T>
by callingnew Schema<T>()
, then generating a model of typeModel<T>
by callingmongoose.model('ModelName', schema)
, the type of the_id
property on a document created via eithernew ModelName()
orawait ModelName.create()
resolves toany
in most situations, instead ofT['_id']
orTypes.ObjectId
as expected.Based on the definition of Mongoose's internal
Require_id<T>
type which is used inHydratedDocument
, I believe the intent is for a document's_id
to never resolve toany
, but rather default toTypes.ObjectId
when an explicit type isn't provided for_id
?This bug appears to be caused by usage of
any
as a default type parameter in the definitions of theDocument
andHydratedDocument
types.I believe it stems from how TypeScript handles
a unionan intersection of two types containing the same property name, where the property isany
in one type and some explicit type in the other.Edit: Forgot to add that there's one specific way of defining the interface that will produce an
_id
property that resolves toTypes.ObjectId
in the resulting documents: If the interface both includes_id: Types.ObjectId
and extendsDocument
. This is demonstrated in the full example further below. I assume that this isn't the intended behavior, as the Mongoose docs recommend against extendingDocument
.Example:
However, using
unknown
instead ofany
generates a different result:Essentially, if I'm understanding this right,
any & T = any
whereasunknown & T = T
. I believe the latter behavior is what's desired in theHydratedDocument
andRequire_id
types provided by Mongoose.Simply editing Mongoose's
index.d.ts
file in node_modules as follows seems to resolve the problem in my testing, but I'm not sure what sort of cascading changes this might cause elsewhere:Before:
After:
If the current behavior is a bug, please provide the steps to reproduce.
tsconfig.json:
What is the expected behavior?
Assuming we have an interface of type
T
, used to create a schema by callingnew Schema<T>()
, which is passed tomongoose.model('ModelName', schema)
to create a model of typeModel<T>
, a new document created by eithernew ModelName()
orawait ModelName.create()
should have an_id
property of either typeT['_id']
ifT extends { _id?: any }
orTypes.ObjectId
otherwise.What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.
Node: 16.10.0
TypeScript: 4.5.3
Mongoose: 6.1.1
MongoDB: 4.2.1
Edit 2: Realized I've been referring to unions when I'm actually talking about intersections. Always reverse those two mentally, oops.
The text was updated successfully, but these errors were encountered: