Property does not exist on populated document mongoose - node.js

Let's say I have two schemas:
Foo.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IFoo extends Document {
name: string;
}
const fooSchema = new Schema(
{
name: {
type: String,
required: true,
}
}
);
export default mongoose.model<IFoo>('Foo', fooSchema);
And Bar.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IBar extends Document {
fooId: string | IFoo; // can be string or can be Foo document
}
const barSchema = new Schema(
{
fooId: {
type: Schema.Types.ObjectId,
ref: 'Foo',
required: true,
},
title: String;
}
);
export default mongoose.model<IBar>('Bar', barSchema);
Now when I find a Bar document with Foo populated. I get a compilation error from typescript
const bar = await Bar.findOne({ title: 'hello' }).populate({ path: 'fooId', model: 'Foo' });
bar.fooId.name // here typescript gives an error
The error is
Property 'name' does not exist on type 'string'
Since I've defined in IBar that fooId can be string | IFoo. Why typescript is complaining? And how to solve it?

If you check typings of the populate method you can see that it's result type is simply this. So, before addressing name field of the fooId property you have to explicitly narrow the type of the result to IFoo type:
const bar = await Bar.findOne({ title: 'hello' })
.populate({ path: 'fooId', model: 'Foo' });
if (typeof bar.fooId !== 'string') { // discards the `string` type
bar.fooId.name
}
Or just simply type assert the result:
const bar = await Bar.findOne({ title: 'hello' })
.populate({ path: 'fooId', model: 'Foo' });
(bar.fooId as IFoo).name

Related

mongoose typescript compilation error with ts-node

I'm getting the error: "Argument of type 'unknown' is not assignable to parameter of type 'string | undefined'"
I'm using mongoose with ts-node
league.model.ts
import { model, Schema, Model, Document } from 'mongoose';
interface ILeague extends Document {
name: string;
}
const LeagueSchema: Schema = new Schema({
name: { type: String, required: true }
});
export const League = model('League', LeagueSchema);
....
await League.findOneAndUpdate(query, league, options)

tsc doesn't recognize virtuals on mongoose scheme

I love mongoose virtual but I can't make it work in typescript.
I'm using mongoose's InferSchemaType to create the interface as described in "another approach:" in mongoose documentation
TSC doesn't recognize them as a field in the interface.
I tried in both suggested manners (see code below).
import {connect, InferSchemaType, Schema, model} from 'mongoose';
const url = 'mongodb://admin:admin#0.0.0.0:27017/';
export const DBS_Actor = new Schema(
{
firstName: String,
lastName: String,
},
{
virtuals: {
fullName: {
get() {
return this.firstName + ' ' + this.lastName;
},
},
},
}
);
DBS_Actor.virtual('tagname').get(function () {
return 'Secrete Agent 007';
});
export type IActor = InferSchemaType<typeof DBS_Actor>;
export const Actor = model<IActor>('User', DBS_Actor);
run().catch(err => console.log(err));
async function run() {
await connect(url);
const actor = new Actor({
firstName: 'jojo',
lastName: 'kiki',
});
await actor.save();
console.log(actor.toJSON()); // {firstName: 'jojo', lastName: 'kiki', _id: new ObjectId("62e52b18d41b2bd4d2bd08d8"), __v: 0 }
console.log(actor.firstName); // jojo
// console.log(actor.fullname); //TSC error TS2339: Property 'fullname' does not exist on typ
// console.log(actor.tagname); //TSC error TS2339: Property 'tagname' does not exist on type...
}
You can extend your type if you want to use additional fields on your type:
export type IActor = InferSchemaType<typeof DBS_Actor> & {
firstName: String
};

Mongoose will not update document with new object property

I have the following Schema:
import Mongoose from 'mongoose'
const ThingSchema = new Mongoose.Schema({
name: {
type: String
},
traits: {
type: Object
}
})
const Thing = Mongoose.model('Thing', ThingSchema)
export default Thing
The first time I created such a document and saved it in the DB I set a name: 'something' and a traits: { propA: 1, propB: 2 } and everything was saved correctly.
Now I want to update the document and set a new property inside traits:
let thingInDB = await ThingModel.findOne({ name: 'something' })
console.log(thingInDB.traits) // <-- it logs { propA: 1, propB: 2 } as expected
thingInDB.traits.propC = 3
await thingInDB.save()
The above code is executed with no errors but when I look in the DB the new propC is not saved in traits. I've tried multiple times.
Am I doing something wrong ?
Have you tried using thingSchema.markModified("trait") before the .save() method? It worked for me when I ran into a similar problem in the past.
I had to declare every property of the object explicitly:
import Mongoose from 'mongoose'
const ThingSchema = new Mongoose.Schema({
name: {
type: String
},
traits: {
propA: {
type: Number
},
propB: {
type: Number
},
propC: {
type: Number
}
}
})
const Thing = Mongoose.model('Thing', ThingSchema)
export default Thing

Mongoose with Typescript: Trying to split schema, methods and statics in seperate files, problem with this has type any and is hidden by container

i'm trying to split up my single-files mongoose schemas with statics and methods.
(I found this tutorial for splitting: https://medium.com/swlh/using-typescript-with-mongodb-393caf7adfef ) I'm new to typescript but love the benefits it gives while coding.
I've splitted my user.ts into:
user.schema.ts
user.model.ts
user.types.ts
user.statics.ts
user.methods.ts
When i change this lines in my schema file:
UserSchema.statics.findUserForSignIn = async function findUserForSignIn(
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ email: email });
if (!user) {
return user;
} else {
return user;
}
}
to UserSchema.statics.findUserForSignIn = findUserForSignIn;
and copy the Function findUserForSignIn to user.statics.ts, Typescript says "'this' implicitly has type 'any' because it does not have a type annotation" and "An outer value of 'this' is shadowed by this container."
So, how to add this properly? If i add this to findUserForSignIn with IUserModel as Type, add null to Promise return type it would nearly work:
export async function findUserForSignIn(
this: IUserModel,
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ "person.email": email });
return user;
}
And if i add this to receiving function parameters: users gets to type IUserDocument, before it was any. I think its nice to have typeclear, not just any.
But: in user.schema.ts the UserSchema.statics.findUserForSignIn gets a red line from typescript. Type can not be assigned to other type. The signature of this is not identical.
If i change the type of this to any, all is okay. But the return is not longer from type IUserDocument. Mabye its okay if i get over an aggregation pipeline and only set the Promise-Return-Type. But that this: any gets hinted in yellow by typescript.
And, another question: if i pass this as first and email as second parameter, why is only one parameter required?
Anyone has an "how to" for me? Or can explain what i've done wrong? Or what is the best way? Or is it not possible to split statics and methods in seperate files from schema?
Original files:
user.schema.ts
import { Schema } from "mongoose";
import { PersonSchema } from "./person.schema";
import { findUserForSignIn } from "./user.statics";
import { IUserDocument } from "./user.types";
const UserSchema = new Schema<IUserDocument>({
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
});
UserSchema.statics.findUserForSignIn = findUserForSignIn;
export default UserSchema;
user.types.ts
import { Document, Model } from "mongoose";
import { IPerson } from "./person.types";
export interface IUser {
firstname: string;
lastname: string;
email: string;
}
export interface IUserDocument extends IUser, Document {}
export interface IUserModel extends Model<IUserDocument> {
findUserForSignIn: (email: string) => Promise<IUserDocument>;
}
user.model.ts
import { model } from "mongoose";
import UserSchema from "./user.schema";
import { IUserDocument, IUserModel } from "./user.types";
const User = model<IUserDocument>("User", UserSchema) as IUserModel;
export default User;
user.statics.ts
import { IUserDocument } from "./user.types";
export async function findUserForSignIn(
email: string
): Promise<IUserDocument | null> {
const user = await this.findOne({ email: email });
if (!user) {
return user;
} else {
return user;
}
}
The only way seems to change the user.statics.ts
export async function findUserForSignIn(
this: Model<IUserDocument>,
email: string
): Promise<IUserDocument | null> {
console.log("E-Mail", email);
const user = await this.findOne({ email: email });
return user;
}
this has to be of type Model
Then code seems to be okay.

How to extend Mongoose Schema in Typescript

I'm making 3 schemas (article, comment, user) and models that share some fields.
FYI, I'm working with mongoose and typescript.
mongoose v6.1.4
nodejs v16.13.1
typescript v4.4.3
interface of each 3 schema shares a common interface UserContent, and they looks like this:
interface IUserContent {
slug: string;
should_show: 'always' | 'never' | 'by_date';
show_date_from: Date | null;
show_date_to: Date | null;
published_date: Date | null;
}
interface IArticle extends IUserContent {
title: string;
content: string;
user_id: number;
}
interface IComment extends IUserContent {
content: string;
user_id: number;
}
interface IUser extends IUserContent {
name: string;
description: string;
}
And I'm trying to make an function which creates Mongoose Schema with shared fields:
import { Schema, SchemaDefinition } from 'mongoose'
const createUserContentSchema = <T extends object>(fields: SchemaDefinition<T>) => {
const schema = new Schema<IUserContent & T>({
// theese fields are shared fields
slug: { type: String },
should_show: { type: String, enum: ['always', 'never', 'by_date'] },
show_date_from: { type: Date },
show_date_to: { type: Date },
published_date: { type: Date },
// this is not-shared fields
...fields,
})
return schema
}
I was expected that this function will create schema with shared fields and non-shared fields combined together. (like code below)
const UserSchema = createUserContentSchema<IUser>({
name: {type: String},
description: {type: String},
});
However, It throws Type Error on the object parameter in new Schema which is inside createUserContentSchema function. (nevertheless compiled javascript code works well as expected)
Type '{ slug: { type: StringConstructor; }; should_show: { type: StringConstructor; enum: string[]; }; show_date_from: { type: DateConstructor; }; show_date_to: { ...; }; published_date: { ...; }; } & SchemaDefinition' is not assignable to type 'SchemaDefinition<SchemaDefinitionType<IUserContent & T>>'.ts(2345)
I removed generic from createUserContentSchema function and directly replaced T to IUser and it turns out to be nice without error. So, I'm assuring that I made mistake in typing generic. but can't figure out what did I make wrong exactly.
I want to fix my code to not make this Type Error.
PS
I found out that my error is reproduced only in mongoose#v6 (not v5)
I read the breaking changes in update note but can't figure why this error is being produced in v6.
Mongoose Discriminator sounds like a feature you need.
https://mongoosejs.com/docs/api.html#model_Model.discriminator
function BaseSchema() {
Schema.apply(this, arguments);
this.add({
name: String,
createdAt: Date
});
}
util.inherits(BaseSchema, Schema);
const PersonSchema = new BaseSchema();
const BossSchema = new BaseSchema({ department: String });
const Person = mongoose.model('Person', PersonSchema);
const Boss = Person.discriminator('Boss', BossSchema);
new Boss().__t; // "Boss". `__t` is the default `discriminatorKey`
const employeeSchema = new Schema({ boss: ObjectId });
const Employee = Person.discriminator('Employee', employeeSchema, 'staff');
new Employee().__t; // "staff" because of 3rd argument above

Resources