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

Moving from plain array to mongoose.Types.DocumentArray #108

Open
Dezzymei opened this issue Dec 12, 2022 · 13 comments
Open

Moving from plain array to mongoose.Types.DocumentArray #108

Dezzymei opened this issue Dec 12, 2022 · 13 comments

Comments

@Dezzymei
Copy link

Dezzymei commented Dec 12, 2022

Hello! I am having issues trying to create a new Document in ts (it works fine in js).

"mongoose": "^5.11.10",
"mongoose-tsgen": "7.1.3",
node: v16.15.1
npm: v8.11.0
tsc: Version 4.9.3

Schema user.ts:

...
tikTokAdvertiserAccounts: [
    {
      id: String,
      name: String,
    },
  ],
...

I get this in my types.gen.ts:

...
export interface UserTikTokAdvertiserAccount {
  id?: string;
  name?: string;
  _id: mongoose.Types.ObjectId;
}
...

I want to create it in my router:

...
let advertiserAccounts: UserTikTokAdvertiserAccount[] = [];
advertiserAccounts.push({id:'testid', name:'testname'});

user.tikTokAdvertiserAccounts = advertiserAccounts; // problem line

await user.save();
...

This throws an error:
Type 'UserTikTokAdvertiserAccount[]' is missing the following properties from type 'DocumentArray<UserTikTokAdvertiserAccountDocument>': isMongooseDocumentArray, create, id, $pop, and 8 more.

So my question is how do I create this DocumentArray?

Interestingly if I try:

...
user.tikTokAdvertiserAccounts = new mongoose.Types.DocumentArray(
    advertiserAccounts
);
...

it seems to work out fine without Typescript complaining.

But if I try something on multiple lines then typescript gets confused:

const explicitAccounts = new mongoose.Types.DocumentArray(
    advertiserAccounts
);
user.tikTokAdvertiserAccounts = explicitAccounts;

Type 'DocumentArray<Document<any, any, any>>' is not assignable to type 'DocumentArray<UserTikTokAdvertiserAccountDocument>'. Type 'Document<any, any, any>' is missing the following properties from type 'UserTikTokAdvertiserAccountDocument': ownerDocument, parent, parentArray

So my question is how should I create these sub document arrays on this user document?

@francescov1
Copy link
Owner

Sorry for the delay @Dezzymei, yes arrays can be tricky here with Typescript.

I find the best thing to do is always use push directly on the Mongoose array.

Eg

user.tikTokAdvertiserAccounts.push ({id:'testid', name:'testname'})

await user.save();

I don't have a computer to test this right now, so let me know if you still have issues and I'll get back to you ASAP.

@francescov1
Copy link
Owner

Mongoose arrays get initiated with an empty array by default, so you don't even need to initialize it to an empty array.

@Dezzymei
Copy link
Author

Dezzymei commented Dec 19, 2022

Not quite convinced this does work though. It kind of removes the type checking that I wanted to get as the following does not throw an error:

user.tikTokAdvertiserAccounts.push ({id:'testid', name:'testname', someNonExistentParameter:'blah'})

The rest of this might be moot then:

Also, how can I make the array empty?

i.e. I would have done:

user.tikTokAdvertiserAccounts = [];

I also have an array of strings in here but I have no idea how to create it from scratch without using something like this:

const strings: string[] = ['hi','there'];
new mongoose.Types.Array(...strings)

(I do realise this is probably more of a question for mongoose than yourself!) Thanks for your help!

@francescov1
Copy link
Owner

Hmm yeah I see your point. You could use the generated array type to ensure type safety, and then push into the Mongoose array like so:

let advertiserAccounts: UserTikTokAdvertiserAccount[] = [];
advertiserAccounts.push({id:'testid', name:'testname'});

user.tikTokAdvertiserAccounts.push(...advertiserAccounts)

for setting to an empty array, you may have to do some type casting, or just as any. Havent explored that too much but can look into it if you are still having issues.

@Dezzymei
Copy link
Author

Dezzymei commented Jan 2, 2023

I don't really like that solution, but I am guessing this is perhaps more of a mongoose problem than your library.

If you do have a solution I would love to implement it because right now it doesn't feel very typesafe for these properties...

@francescov1
Copy link
Owner

Yes unfortunately this is something I haven't been able to get around with Mongoose. I would look into the Mongoose docs for Typescript usage, its possible that things have changed since I last dove into it.

In terms of type safety, I think the solution above is decently safe; you are still using UserTikTokAdvertiserAccount to ensure the items in the array are of the correct type. But its possible to forget to use this type, so definitely not a perfect solution.

@Dezzymei
Copy link
Author

Dezzymei commented Jan 3, 2023

I raised a stackoverflow to see if the world can help me! :)

https://stackoverflow.com/questions/74997609/mongoose-documentarray-types-with-autogenerated-mongoose-typescript-types-with

@maxshugar
Copy link

Hey, great library! @francescov1

Any update on this issue? @Dezzymei

@maxshugar
Copy link

maxshugar commented Jul 10, 2024

I have the following model:

const venueSchema: Schema = new Schema({
	openingHours: [
		{
			day: {
				type: String,
				required: true,
			},
			active: {
				type: Boolean,
				required: true,
			},
			openingTime: {
				type: String,
				required: true,
			},
			closingTime: {
				type: String,
				required: true,
			},
		},
	],
});

Which generates:

export type VenueDocument = mongoose.Document<
  mongoose.Types.ObjectId,
  VenueQueries
> &
  VenueMethods & {
    openingHours: mongoose.Types.DocumentArray<VenueOpeningHourDocument>;
    _id: mongoose.Types.ObjectId;
  };

When I try and set the value of opening hours:

const venue = await VenueModel.findById(venueId);
venue.openingHours = [
		{
			day: 'Monday',
			active: true,
			openingTime: "09:00",
			closingTime: "17:00",
		}
	];

I receive a similar error:

Type '{ day: string; active: true; openingTime: string; closingTime: string; }' is not assignable to type 'VenueOpeningHourDocument'.
  Type '{ day: string; active: true; openingTime: string; closingTime: string; }' is missing the following properties from type 'Subdocument<ObjectId, any, any>': $isSingleNested, ownerDocument, parent, $parent, and 50 more.

@francescov1
Copy link
Owner

hey @maxshugar let me revisit this in the next few days, i'll see if there's been any updates to Mongoose typing to provide a better solution here.

@francescov1
Copy link
Owner

francescov1 commented Jul 18, 2024

Looks like the way Mongoose does it natively is to set subdocument types on schemas to a regular object, rather than the true Subdocument type. Then they ask users to explicitly override these with the correct subdocument types if they need those. See here: https://mongoosejs.com/docs/typescript/subdocuments.html. This allows users to be able to set a subdocument to a plain JS object without JS complaining, and then under the hood Mongoose manually converts the JS object to a subdocument. I dont really like that solution because its incorrectly typing the subdocument on the schema, its missing all the helper methods that a usual subdocument would have.

The types generated by mongoose-tsgen are more correct, but the downside is they prevent you from using the shorthand form mentioned above. I'd actually argue this is actually good thing, it enforces more explicit code which makes clear that you aren't overwriting a subdocument with a plain JS object. And according to the Mongoose docs, the recommended way to create subdoc arrays is with the push method or create method. So in the last example from @maxshugar, this would look like this:

// If you want to overwrite the whole subdoc, like you were doing before, use create:
const venue = await VenueModel.findById(venueId);
const openingHour = venue.openingHours.create({
	day: 'Monday',
	active: true,
	openingTime: "09:00",
	closingTime: "17:00",
});
venue.openingHours = [openingHour]

// Or to just append a document (even if the openingHours list is empty):
venue.openingHours.push({
	day: 'Monday',
	active: true,
	openingTime: "09:00",
	closingTime: "17:00",
});

Now I'd rather make mongoose-tsgen more flexible rather than less, and don't like impose coding structure on existing repos, but I dont see any way to accomplish this request without forgoing type safety. The only thing that could accomplish it without reducing type safety is if Typescript had a way to define a type for setting a field, and a different type for getting the same field, but I haven't seen anything like that

@francescov1
Copy link
Owner

Let me know your thoughts @Dezzymei @maxshugar, happy to explore other options if you have any ideas, otherwise will close this out if the solution above works

@maxshugar
Copy link

Thanks @francescov1, I don't mind not using the short hand form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants