-
-
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
Correctly typing single Nested Paths and Subdocuments #11061
Comments
Did some more investigation. There is also the possibility to use But there are still a couple of issues here. Setup: import mongoose, { HydratedDocument, Types, Schema } from 'mongoose';
interface Names {
firstName: string;
}
interface User {
names: Names;
}
type UserDocument = HydratedDocument<
User & {
names: Types.Subdocument;
}
>;
const userSchema = new Schema<User>({
names: new Schema<Names>({ firstName: String }) // You have to use new Schema and not a nested path
});
const UserModel = mongoose.model<UserDocument>('User', userSchema); Issue 1: TS allows to set the firstName field on the names object, even though names in this case is const user = new UserModel({});
user.names === undefined; // true, as detailed here https://mongoosejs.com/docs/subdocs.html#subdocuments-versus-nested-paths
user.names.firstName = 'John'; // TypeError: Cannot set properties of undefined Issue 2: Assignment on the subdocument is correctly handled in JS but TS is throwing errors. const user = new UserModel({});
user.names = { firstName: 'John' }; // Type '{ firstName: string; }' is missing the following properties from type 'Subdocument': $isSingleNested, ownerDocument, parent, $parent, and 49 more
console.log(user.names.constructor.name); // correctly SingleNested and not just a simple object with { firstName: 'John' } Issue 3: The nested object is of class const user = new UserModel({ names: {} }); // <- now names is defined as empty object
user.names.constructor.name; // SingleNested Issue 4: The When the interface Names {
_id: Types.ObjectId;
firstName: string;
}
// ...rest of setup
const user = new UserModel({ names: {} });
user.names._id; // (property) _id: any When the interface Names {
firstName: string;
}
// ...rest of setup
const user = new UserModel({ names: {} });
user.names._id; // (property) Document<any, any, any>._id?: any Issue 5: Although using // When using UserDocument that was created with `HydratedDocument`
const UserModel = mongoose.model<UserDocument>('User', userSchema);
const user = await UserModel.find({ baseModelName: 'stuff' }) // <- TS thinks this is a valid query but it's not I'm still unsure whether I'm going in the right direction or not, and I still don't have a great way to type nested paths. |
Hey @janos-kasa did you manage to find a solution? I have actually tried copying your example from gist number 3, but typescript is giving me a whole bunch of errors, I'm using the latest version of mongoose (6.1.4) at the time of writing this. Argument of type 'Schema<User, Model<User, any, any, any>, any>' is not assignable to parameter of type 'Schema<Document<any, any, User & { names: Document<any, any, Names> & Names & { _id: ObjectId; }; }> & User & { names: Document<...> & ... 1 more ... & { ...; }; } & { ...; }, any, any> | Schema<...>'.
Type 'Schema<User, Model<User, any, any, any>, any>' is not assignable to type 'Schema<Document<any, any, User & { names: Document<any, any, Names> & Names & { _id: ObjectId; }; }> & User & { names: Document<...> & Names & { ...; }; } & { ...; }, any, any>'.
Types of property 'add' are incompatible.
Type '(obj: { names?: SchemaDefinitionProperty<Names>; } | Schema<any, Model<any, any, any, any>, any>, prefix?: string) => Schema<User, Model<...>, any>' is not assignable to type '(obj: Schema<any, Model<any, any, any, any>, any> | { names?: SchemaDefinitionProperty<Names & Document<any, any, Names> & { _id: ObjectId; }>; _id?: SchemaDefinitionProperty<...>; __v?: SchemaDefinitionProperty<...>; id?: SchemaDefinitionProperty<...>; }, prefix?: string) => Schema<...>'.
Types of parameters 'obj' and 'obj' are incompatible.
Type 'Schema<any, Model<any, any, any, any>, any> | { names?: SchemaDefinitionProperty<Names & Document<any, any, Names> & { _id: ObjectId; }>; _id?: SchemaDefinitionProperty<...>; __v?: SchemaDefinitionProperty<...>; id?: SchemaDefinitionProperty<...>; }' is not assignable to type '{ names?: SchemaDefinitionProperty<Names>; } | Schema<any, Model<any, any, any, any>, any>'.
Type '{ names?: SchemaDefinitionProperty<Names & Document<any, any, Names> & { _id: ObjectId; }>; _id?: SchemaDefinitionProperty<any>; __v?: SchemaDefinitionProperty<...>; id?: SchemaDefinitionProperty<...>; }' is not assignable to type '{ names?: SchemaDefinitionProperty<Names>; } | Schema<any, Model<any, any, any, any>, any>'.
Type '{ names?: SchemaDefinitionProperty<Names & Document<any, any, Names> & { _id: ObjectId; }>; _id?: SchemaDefinitionProperty<any>; __v?: SchemaDefinitionProperty<...>; id?: SchemaDefinitionProperty<...>; }' is not assignable to type '{ names?: SchemaDefinitionProperty<Names>; }'.
Types of property 'names' are incompatible.
Type 'SchemaDefinitionProperty<Names & Document<any, any, Names> & { _id: ObjectId; }>' is not assignable to type 'SchemaDefinitionProperty<Names>'.
Type 'SchemaTypeOptions<Names & Document<any, any, Names> & { _id: ObjectId; }>' is not assignable to type 'SchemaDefinitionProperty<Names>'.
Type 'SchemaTypeOptions<Names & Document<any, any, Names> & { _id: ObjectId; }>' is not assignable to type 'SchemaTypeOptions<Names>'.
Types of property 'validate' are incompatible.
Type 'Function | RegExp | [RegExp, string] | [Function, string] | ValidateOpts<Names & Document<any, any, Names> & { _id: ObjectId; }> | ValidateOpts<...>[]' is not assignable to type 'Function | RegExp | [RegExp, string] | [Function, string] | ValidateOpts<Names> | ValidateOpts<Names>[]'.
Type 'ValidateOpts<Names & Document<any, any, Names> & { _id: ObjectId; }>' is not assignable to type 'Function | RegExp | [RegExp, string] | [Function, string] | ValidateOpts<Names> | ValidateOpts<Names>[]'.
Type 'ValidateOpts<Names & Document<any, any, Names> & { _id: ObjectId; }>' is not assignable to type 'ValidateOpts<Names>'.
Type 'Names' is not assignable to type 'Names & Document<any, any, Names> & { _id: ObjectId; }'. So not even sure where to begin with this... In my example I am trying to do something similar, but I am not sure if this is the correct way to do this. import { Schema, Types, model } from 'mongoose';
interface ITeam {
id: number;
name: string;
}
export interface IMatch {
_id: Types.ObjectId;
id: number;
homeTeam: ITeam;
awayTeam: ITeam;
}
const TeamSchema = new Schema<ITeam>({
id: { type: Number, required: true, index: true },
name: { type: String, required: true },
})
const MatchSchema = new Schema<IMatch>(
{
_id: { type: Schema.Types.ObjectId, auto: true },
id: { type: Number, required: true, unique: true },
homeTeam: { type: TeamSchema, required: true },
awayTeam: { type: TeamSchema, required: true },
}
);
export const Matches = model<IMatch>('matches', MatchSchema); I have also noticed that if you try to add subdocument defaults then TypeScript will give you errors. const TeamSchema = new Schema<ITeam>({
id: { type: Number, required: true, index: true, default: 1 },
name: { type: String, required: true, default: 'Team Name' },
})
const MatchSchema = new Schema<IMatch>(
{
_id: { type: Schema.Types.ObjectId, auto: true },
id: { type: Number, required: true, unique: true },
homeTeam: { type: TeamSchema, required: true, default: () => ({}) },
awayTeam: { type: TeamSchema, required: true, default: () => ({}) },
}
); Type '{ type: Schema<ITeam, Model<ITeam, any, any, any>, any>; required: true; default: () => {}; }' is not assignable to type 'SchemaDefinitionProperty<ITeam>'.
Types of property 'default' are incompatible.
Type '() => {}' is not assignable to type 'ITeam | ((this: any, doc: any) => ITeam)'.
Type '() => {}' is not assignable to type '(this: any, doc: any) => ITeam'.
Type '{}' is missing the following properties from type 'ITeam': id, name
Type '{ type: Schema<ITeam, Model<ITeam, any, any, any>, any>; required: true; default: () => {}; }' is not assignable to type 'SchemaDefinitionProperty<ITeam>'.
Types of property 'default' are incompatible.
Type '() => {}' is not assignable to type 'ITeam | ((this: any, doc: any) => ITeam)'.
Type '() => {}' is not assignable to type '(this: any, doc: any) => ITeam'.
Type '{}' is missing the following properties from type 'ITeam': id, name I suppose the error makes sense since these are required properties of |
Going through the issues on this comment: #11061 (comment) Issue 1: Expected behavior. TypeScript doesn't catch this potential error case. Below example also compiles fine interface Names {
firstName: string;
}
interface User {
names?: Names;
}
const doc: User = {};
doc.names.firstName = 'foo'; Issue 2: See discussion on issue 5 Issue 3: This is a naming inconsistency. Issue 4: Do this instead: interface Names {
_id: Types.ObjectId;
firstName: string;
}
interface User {
names: Names;
}
type UserDocument = HydratedDocument<
User & {
names: Types.Subdocument & Names;
}
>; We'll add a generic param to Issue 5: This is why we typically don't recommend passing a class that @barca-reddit you can work around this by pulling the subdocument defaults into the |
…returning `Partial<T>` from default functions for nested defaults Re: #11061
@janos-kasa I took a closer look, the best approach I've been able to find is to use a document interface that doesn't interface Names {
_id: Types.ObjectId;
firstName: string;
}
interface User {
names: Names;
}
type UserDocument = User & { names: Types.Subdocument & Names; }; Mongoose's typings are smart enough to handle switching between We'll keep this issue open so we take another look later and add some info on this to the docs, but right now 0ad0d94 and the suggestion to use |
We clarified how to handle subdocs in TypeScript in our docs, you can read the markdown here. The changes will be live on our site when we ship our next release. |
Do you want to request a feature or report a bug?
Feature (?)
What is the current behavior?
I'm currently unsure on how to type single nested paths or subdocuments correctly with typescript and it would be awesome to get some clarification around this.
First things first: I am trying to avoid using
extends Document
because of the reasons mentioned here.My other goal is to export type definitions of the plain JS version of the document (i.e. what you would get after a
.toObject()
call), and the type of the document instance too, since they can be both useful for our applications (like as parameters to a function).I've also read the instructions on how to correctly type arrays of subdocuments, but haven't found any on single subdocuments.
So I have created three gists to showcase how I approached the problem, and what worked and what not.
Version 1 (using a standard interface):
When having a single nested subdocument by only supplying the
User
interface the subdocument itself won't get all the methods that are actually available on the subdocument instance. If you try that code with JS it will correctly work, but TS is throwing errors.Version 2 (using HydratedDocument):
The same thing happens if it's typed with the
HydratedDocument
type but the subdocument is still just a standard interface.Version 3 (using HydratedDocument on the subdocument also):
This works for me, and it's also a good thing that this way I can also export the "raw" interfaces that don't have any mongoose specific fields on them, but not sure this is the correct solution as I've never seen this documented anywhere. I'm also not sure whether the subdocument will now have any methods on it that it shouldn't (the
.db
field being available looks a bit weird for example).Could you please help us figure out how to type these correctly? Also if there is a difference between typing a "nested path" and a "subdocument" please let us know!
Thanks!
tsconfig.json
What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.
Mongoose version: v6.0.14
Node version v14.17.0
Mongo version: v4.4.10
The text was updated successfully, but these errors were encountered: