number saved as string in Mongoose MongoDB SubDocument

I experienced a weird error when I was converting one of my Spring Data MongoDB classes to Mongoose. Sometimes Mongoose saves the number attribute as string in MongoDB and then it performs string concatenation instead of performing mathematics addition or subtraction when I read it back and do some calculations. It rattled me for almost whole Saturday that I almost lost confidence on Mongoose or even Javascript and doubted myself whether using Javascript a weakly typed language to develop backend service is a good choice or not. I never had this kind of problem in Java before.

My simplified annotated Mongoose classes are:

			@Schema({
			    collection: 'trade',
			})        
			export class Trade {
			    @Prop() accountId : string;
			    @Prop() symbol : string;
			    @Prop() entryTransaction : Transaction;
			    @Prop() exitTransaction : Transaction;
			    ...
			}
			
			export class Transaction {
			    @Prop() timestampString : string; //Timestamp in YYYY-MM-DD hh:mm:ss format. It is always Eastern Time.
			    @Prop() shares : number;
			    @Prop() price : number;
			    @Prop() fee : number;
			    @Prop() amount : number; 
			    ...
			}        
        
These two classes are translated from Spring Data Model annotated Java classes directly. I have been doing this for quite a few days and I can insert, update, query and delete document in MongoDB without any problem until I noticed something really weird.

Here is my test case:

  1. At 9:30 AM, I bought 200 shares of stock ABCD at price $20.
  2. At 13:34 PM, I bought 100 shares of stock ABCD at price $20

  3. 					{ "_id" : ObjectId("6221344e970489a48825df2c"), "holdAmount" : 4000, "holdShares" : 200, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 20, "fee" : 0, "amount" : 4000, "shares" : 200, "timestampString" : "2022-03-03 09:30:00", "tradeTransactionId" : "6221344e970489a48825df2a" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0 }
    					{ "_id" : ObjectId("62213471970489a48825df31"), "holdAmount" : 1000, "holdShares" : 100, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 10, "fee" : 0, "amount" : 1000, "shares" : 100, "timestampString" : "2022-03-03 13:34:00", "tradeTransactionId" : "62213471970489a48825df2f" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0 }                
                    
  4. Now I tried to book a sell transaction happened at 11:37 AM where I sold 100 shares ABCD at $15. Since this transaction happened before the second BUY transaction, I need to find the first transaction and deduct 100 shares there. On the screen it looks fine. There is 100 shares left at $20 entry price at 9:30 AM, and the other 100 shares left at $10 entry price at 13:34 PM.

    However by looking at MongoDB, we can see the shares is saved as "shares" : "100". This is clearly wrong because shares is a number instead of string in my Transaction TypeScript/Javascript class.
    					{ "_id" : ObjectId("6221344e970489a48825df2c"), "holdAmount" : 0, "holdShares" : 0, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "shares" : "100", "timestampString" : "2022-03-03 09:30:00", "price" : 20, "fee" : 0, "amount" : 2000, "trueRangeOfDay" : null, "highOfDay" : null, "lowOfDay" : null, "triggeredBySimulator" : null, "tradeTransactionId" : "6221344e970489a48825df2a" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0, "exitMonth" : 3, "exitTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 15, "fee" : 0, "amount" : 1500, "shares" : "100", "timestampString" : "2022-03-03 11:37:00", "tradeTransactionId" : "62213528970489a48825df35" }, "exitYear" : 2022, "holdDays" : 0, "profit" : -500, "profitPercent" : -25 }
    					{ "_id" : ObjectId("62213471970489a48825df31"), "holdAmount" : 1000, "holdShares" : 100, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 10, "fee" : 0, "amount" : 1000, "shares" : 100, "timestampString" : "2022-03-03 13:34:00", "tradeTransactionId" : "62213471970489a48825df2f" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0 }
    					{ "_id" : ObjectId("62213528970489a48825df3a"), "profitPercent" : 0, "profit" : 0, "holdAmount" : 2000, "holdShares" : 100, "exitTransaction" : null, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "shares" : 100, "timestampString" : "2022-03-03 09:30:00", "price" : 20, "fee" : 0, "amount" : 2000, "trueRangeOfDay" : null, "highOfDay" : null, "lowOfDay" : null, "triggeredBySimulator" : null, "tradeTransactionId" : "6221344e970489a48825df2a" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0 }
                    
  5. Without noticing it on the screen, I continue to sell 150 shares ABCD at 15:39 at $16. My application is designed to find the lowest entry price shares and sell them first, and then find the next lowest entry price shares to sell. On the screen there is 50 shares bought at 9:30 AM left. It looks fine at this moment although the data saved in MongoDB continued to be messed up.

    					{ "_id" : ObjectId("6221344e970489a48825df2c"), "holdAmount" : 0, "holdShares" : 0, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "shares" : "100", "timestampString" : "2022-03-03 09:30:00", "price" : 20, "fee" : 0, "amount" : 2000, "trueRangeOfDay" : null, "highOfDay" : null, "lowOfDay" : null, "triggeredBySimulator" : null, "tradeTransactionId" : "6221344e970489a48825df2a" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0, "exitMonth" : 3, "exitTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 15, "fee" : 0, "amount" : 1500, "shares" : "100", "timestampString" : "2022-03-03 11:37:00", "tradeTransactionId" : "62213528970489a48825df35" }, "exitYear" : 2022, "holdDays" : 0, "profit" : -500, "profitPercent" : -25 }
    					{ "_id" : ObjectId("62213471970489a48825df31"), "holdAmount" : 0, "holdShares" : 0, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 10, "fee" : 0, "amount" : 1000, "shares" : 100, "timestampString" : "2022-03-03 13:34:00", "tradeTransactionId" : "62213471970489a48825df2f" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0, "exitMonth" : 3, "exitTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "shares" : 100, "timestampString" : "2022-03-03 15:39:00", "price" : 16, "fee" : 0, "amount" : 1600, "trueRangeOfDay" : null, "highOfDay" : null, "lowOfDay" : null, "triggeredBySimulator" : null, "tradeTransactionId" : "6221358e970489a48825df41" }, "exitYear" : 2022, "holdDays" : 0, "profit" : 600, "profitPercent" : 60 }
    					{ "_id" : ObjectId("62213528970489a48825df3a"), "profitPercent" : -20, "profit" : -200, "holdAmount" : 0, "holdShares" : 0, "exitTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "price" : 16, "fee" : 0, "amount" : 800, "shares" : 50, "timestampString" : "2022-03-03 15:39:00", "tradeTransactionId" : "6221358e970489a48825df41" }, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "shares" : 50, "timestampString" : "2022-03-03 09:30:00", "price" : 20, "fee" : 0, "amount" : 1000, "trueRangeOfDay" : null, "highOfDay" : null, "lowOfDay" : null, "triggeredBySimulator" : null, "tradeTransactionId" : "6221344e970489a48825df2a" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0, "exitMonth" : 3, "exitYear" : 2022, "holdDays" : 0 }
    					{ "_id" : ObjectId("6221358e970489a48825df47"), "profitPercent" : 0, "profit" : 0, "holdAmount" : 1000, "holdShares" : 50, "exitTransaction" : null, "entryTransaction" : { "accountId" : "5f5cfef841895f459b5143b9", "symbol" : "ABCD", "shares" : 50, "timestampString" : "2022-03-03 09:30:00", "price" : 20, "fee" : 0, "amount" : 1000, "trueRangeOfDay" : null, "highOfDay" : null, "lowOfDay" : null, "triggeredBySimulator" : null, "tradeTransactionId" : "6221344e970489a48825df2a" }, "symbol" : "ABCD", "accountId" : "5f5cfef841895f459b5143b9", "__v" : 0 }
                    
  6. Now let's delete the first SELL transaction which is 100 shares sold at 11:37 AM. This 100 shares should be added back to the remaining 50 shares bought at 9:30AM, and I should get 150 shares bought at $20 at 9:30 AM. Instead I get 10050 shares because Mongoose returns "shares" : "100" in the exitTransaction, Typescript/Javascript now changes the shares in the exitTransaction object from number to string, and then perform string concatenation instead of mathematics addition when I try to add the share back to original BUY transaction.

What's really going on here? Why the shares property is saved correctly as number the first time or a few times correctly in the database? Why suddenly Mongoose decides to change the number type to string type? Do I have to use the strong typed Number and BigInt class introduced in es2020? To be honest, I converted all the number to Number in these 2 classes and find out it was so inconvenient to use them because TypeScript/Javascript does not support autoboxing like Java does on primitive classes such as Int, BigInt, Double etc.

I spent almost whole afternoon googling around, trying different solution until suddenly I felt I should read Mongoose document regarding Nested Class and SubDocument again carefully because there was a whole chapter dedicated to SubClass, one-to-one, one-to-many mappings in Hibernate and JPA world. After half hour I got everything working perfectly with new classes definition:

            @Schema({
                collection: 'trade',
            })        
            export class Trade {
                @Prop() accountId : string;
                @Prop() symbol : string;
                @Prop({Schema: TransactionSchema}) entryTransaction : Transaction;
                @Prop({Schema: TransactionSchema}) exitTransaction : Transaction;
                ...
            }
            
            @Schema({_id : false})
            export class Transaction {
                @Prop() timestampString : string; //Timestamp in YYYY-MM-DD hh:mm:ss format. It is always Eastern Time.
                @Prop() shares : number;
                @Prop() price : number;
                @Prop() fee : number;
                @Prop() amount : number; 
                ...
            }        
        

Life is good again after I figured out the solution! This time I decided to document it because I couldn't find much useful information on internet. Maybe everybody else just followed Mongoose documentation carefully except me? I can bite the bullet because these days I don't necessary finish reading all the documents or a book before I start to try out a new language or a new framework. I always leverage my experiences on similar stuff somewhere else which served me pretty well and cut my learning time from weeks to days until this weird bug happens. I can only say it was a fun afternoon...