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

Why findById returns (User & { _id: Schema.Types.ObjectId; }) | null they User | null #10987

Closed
Darfion opened this issue Nov 18, 2021 · 7 comments
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary

Comments

@Darfion
Copy link

Darfion commented Nov 18, 2021

This is my first time using typescript and mongoose. Here's my code

type

export interface User extends Document {
  _id: ObjectId;
  lastName: string;
}

Schema

const userSchema = new Schema<User>({
  lastName: { type: String, required: true, trim: true },
});

model

const User = model<User>('user', UserSchema, 'users');

request

const user = await User.findById(userId).exec();

I expect the user variable to be of type User | null.but i get (User & {_id: Schema.Types.ObjectId;}) | null. If I don't declare _id: ObjectId; then I get ( User & { _id: any; }) | null. I do not know if this is some kind of mistake or it should be like this

@vkarpov15
Copy link
Collaborator

What is ObjectId in your document definition, Types.ObjectId or Schema.Types.ObjectId?

@vkarpov15 vkarpov15 added the help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary label Nov 21, 2021
@Darfion
Copy link
Author

Darfion commented Nov 25, 2021

that's how I do the import import { Document, Model, ObjectId } from 'mongoose'; hence type ObjectId = mongoose.Schema.Types.ObjectId

@vkarpov15 vkarpov15 added this to the 6.0.17 milestone Nov 29, 2021
@DavideViolante
Copy link
Contributor

I've the same code except that _id is any in the interface, the return type of a findOne is:

const user: IUser & {
    _id: any;
}

I think it should be IUser, where this & comes from?

@drewkht
Copy link

drewkht commented Dec 16, 2021

I've the same code except that _id is any in the interface, the return type of a findOne is:

const user: IUser & {
    _id: any;
}

I think it should be IUser, where this & comes from?

The return type comes from the internal types used by mongoose.model() to create the User model.

Mongoose's HydratedDocument<T> type is used for all documents created/queried by a model of type Model<T>. It's explained further in the linked Mongoose docs.

Because the User interface extends Document, HydratedDocument<User> resolves to the Mongoose internal type Require_id<User>, which simply creates a type union intersection User & { _id: User['_id'] } (to, I assume, ensure that the _id property is non-nullable - as the _id property on Mongoose's Document type is nullable).

User & { _id: Schema.Types.ObjectId } is a functionally identical type to User, because User already contains the property _id: Schema.Types.ObjectId. It definitely looks weird if you're not familiar with what's going on behind the scenes or with TypeScript unions intersections, though.

@Darfion - I believe if you just change the _id property in the User interface to Types.ObjectId instead you'll get the proper results (i.e. const user will be of type HydratedDocument<User, ...> | null). Types.Schema.ObjectId is only intended to be used within schema definitions, from what I understand.

Also, a couple potential issues in the provided examples:

export interface User extends Document { ... }
const User = model<User>('user', UserSchema, 'users');

At least in my VS Code with my tsconfig.json, this causes a naming conflict between the User interface and const User, resulting in the latter resolving to any. Either renaming the interface to something like IUser or const User to something like const UserModel would solve this problem.

const userSchema = new Schema<User>({ ... });
const User = model<User>('user', UserSchema, 'users');

In the above, userSchema is used to create the schema, but then UserSchema (with an uppercase U) is passed to the model function. Because you're passing User as a type parameter to the model call (which isn't necessary if you're passing the type to Schema already) you'll still get a correctly typed Model from it, but I'm not sure what other issues might arise from not providing an actual Schema to the function.

I rewrote the example like this and it works as expected in my testing:

import { Types, model, Document, Schema } from 'mongoose';

export interface IUser extends Document {
  _id: Types.ObjectId;
  lastName: string;
}

const userSchema = new Schema<IUser>({
  lastName: { type: String, required: true, trim: true },
});

const User = model('user', userSchema, 'users');

const user = await User.findById(userId).exec();   // user: HydratedDocument<IUser, {}, {}> | null
if (user) user._id;   // user._id: Types.ObjectId

@DavideViolante
Copy link
Contributor

Thanks for explanation. I think on line 8 you mean new Schema<IUser>.

@drewkht
Copy link

drewkht commented Dec 17, 2021

I did, thanks! Fixed it. Also fixed where I'd been referring to TypeScript unions (which is T1 | T2) when I should've been saying intersections (T1 & T2). Always reverse them in my head.

@vkarpov15
Copy link
Collaborator

@drewkht 's comment #10987 (comment) is correct. We added a note about the Schema.Types.ObjectId vs Types.ObjectId distinction in #10949

@vkarpov15 vkarpov15 removed this from the 6.1.4 milestone Dec 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary
Projects
None yet
Development

No branches or pull requests

4 participants