[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-content-how-typescript-fooled-me-in-2024":3},"\u003Ch3 id=\"how-typescript-fooled-me-in-2024\">How TypeScript Fooled Me in 2024\u003C\u002Fh3>\u003Ch4 id=\"thanks-to-legacy-code-and-lazy-practices\">Thanks to legacy code and lazy practices.\u003C\u002Fh4>\u003Cp>In this post, we are going to learn (or refresh) some basic TypeScript knowledge and see how subtle changes in third-party libraries can cause big surprises.\u003C\u002Fp>\u003Ch3 id=\"the-problem\">The problem\u003C\u002Fh3>\u003Cp>We’ve added interfacing with Stripe for payments inside our new product. While testing the changes I got a weird error from Stripe, complaining about a value of type buffer not being a string which clearly was supposed to be a string. As it worked perfectly on my colleague's machine before updating node packages, there might have been a change in the Stripe API to suddenly be more strict about values that are given to their API endpoints.\u003C\u002Fp>\u003Cp>So let’s continue to build a showcase of the issue.\u003C\u002Fp>\u003Ch3 id=\"sample-project\">Sample Project\u003C\u002Fh3>\u003Cp>In general, we use NestJS with TypeScript for our backends these days.\u003Cbr>Most of the time we use MongoDB for our databases and work with Mongoose. In order to showcase what I fell victim to, while developing the latest features for our brand-new product \u003Ca href=\"https:\u002F\u002Fcroppy.at\u002F\" rel=\"noopener\">Croppy\u003C\u002Fa>, we’ll set up a sample project.\u003C\u002Fp>\u003Ch4 id=\"initial-setup\">\u003Cstrong>Initial setup\u003C\u002Fstrong>\u003C\u002Fh4>\u003Cp>\u003Cem>We will rely on packages installed as per my \u003C\u002Fem>\u003Ca href=\"\u002Fde-at\u002Fblog\u002Fmacbook-setup-guide-for-developers-2024-edition\">\u003Cem>MacBook Setup Guide\u003C\u002Fem>\u003C\u002Fa>\u003Cem>, so feel free to check it out if you haven’t seen it yet.\u003C\u002Fem>\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-bash\">nvm use lts\u002Firon \nnpm i -g @nestjs\u002Fcli \nnest new --strict tsfoobar \nyarn add @nestjs\u002Fmongoose mongoose \nnest g resource\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>\u003Cem>Note: NestJS commands are interactive. I chose yarn as a package manager when creating the project, called our resource \"cats\", went with a REST API resource, and chose to generate CRUD operations.\u003C\u002Fem>\u003C\u002Fp>\u003Ch4 id=\"adding-a-mongodb-connection\">\u003Cstrong>Adding a MongoDB connection\u003C\u002Fstrong>\u003C\u002Fh4>\u003Cp>As per the docs, we add the MongoDB connection to the app module like this:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-typescript\">import { Module } from '@nestjs\u002Fcommon'; \nimport { MongooseModule } from '@nestjs\u002Fmongoose'; \nimport { AppController } from '.\u002Fapp.controller'; \nimport { AppService } from '.\u002Fapp.service'; \nimport { CatsModule } from '.\u002Fcats\u002Fcats.module'; \n \n@Module({ \n  imports: [MongooseModule.forRoot('mongodb:\u002F\u002Fnest:nest@127.0.0.1:27017\u002Fnest'), \n            CatsModule], \n  controllers: [AppController], \n  providers: [AppService], \n}) \nexport class AppModule {}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch4 id=\"adding-a-mongoose-model-as-per-our-legacy-code\">\u003Cstrong>Adding a Mongoose model (as per our legacy code)\u003C\u002Fstrong>\u003C\u002Fh4>\u003Cpre>\u003Ccode class=\"language-typescript\">import { Prop, Schema, SchemaFactory } from '@nestjs\u002Fmongoose'; \nimport { HydratedDocument, Document } from 'mongoose'; \n \n@Schema() \nexport class Cat { \n  _id: Types.ObjectId; \n \n  @Prop() \n  name: string; \n \n  @Prop() \n  age: number; \n \n  @Prop() \n  breed: string; \n} \n \nexport type CatDocument = Cat &amp; Document; \nexport const CatSchema = SchemaFactory.createForClass(Cat);\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch4 id=\"adding-a-service\">Adding a service\u003C\u002Fh4>\u003Cpre>\u003Ccode class=\"language-typescript\">import { Model } from 'mongoose'; \nimport { Injectable } from '@nestjs\u002Fcommon'; \nimport { InjectModel } from '@nestjs\u002Fmongoose'; \nimport { CreateCatDto } from '.\u002Fdto\u002Fcreate-cat.dto'; \nimport { Cat } from '.\u002Fschemas\u002Fcat.schema'; \n \n@Injectable() \nexport class CatsService { \n  constructor(@InjectModel(Cat.name) private catModel: Model&lt;Cat&gt;) {} \n \n  async create(createCatDto: CreateCatDto): Promise&lt;Cat&gt; { \n    const createdCat = new this.catModel(createCatDto); \n    return createdCat.save(); \n  } \n \n  async findAll() { \n    return this.catModel.find().exec(); \n  } \n \n  async testCall(catId: string): Promise&lt;string&gt; { \n    console.log(catId); \n    return catId; \n  } \n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch4 id=\"adding-a-controller\">Adding a controller\u003C\u002Fh4>\u003Cpre>\u003Ccode class=\"language-typescript\">import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs\u002Fcommon'; \nimport { CatsService } from '.\u002Fcats.service'; \nimport { CreateCatDto } from '.\u002Fdto\u002Fcreate-cat.dto'; \nimport { CatDocument } from '.\u002Fschemas\u002Fcat.schema'; \n \n@Controller('cats') \nexport class CatsController { \n  constructor(private readonly catsService: CatsService) {} \n \n  @Post() \n  create(@Body() createCatDto: CreateCatDto) { \n    return this.catsService.create(createCatDto); \n  } \n \n  @Get() \n  findAll() { \n    return this.catsService.findAll(); \n  } \n \n  @Get(\"surprise\") \n  async surprise() { \n    const cats = await this.catsService.findAll(); \n    const firstcat:CatDocument = cats[0]; \n    this.catsService.testCall(firstcat._id); \n    return cats[0]; \n  } \n}\u003C\u002Fcode>\u003C\u002Fpre>\u003Chr>\u003Cp>\u003Cem>You can find the full project on GitHub: \u003C\u002Fem>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fa1rwulf\u002Ftsfoobar\" rel=\"nofollow noopener noopener noopener\">\u003Cem>https:\u002F\u002Fgithub.com\u002Fa1rwulf\u002Ftsfoobar\u003C\u002Fem>\u003C\u002Fa>\u003C\u002Fp>\u003Ch3 id=\"surprise-surprise\">Surprise Surprise\u003C\u002Fh3>\u003Cp>Ok, so we have the basic setup.\u003C\u002Fp>\u003Cp>People with profound knowledge of TypeScript, NestJS, and Mongoose might have already spotted the issue at hand. But here is what caused a pretty huge what the heck moment for me:\u003C\u002Fp>\u003Cp>\u003Cstrong>WAT!?\u003C\u002Fstrong>\u003Cbr>Several questions hit my mind at the very same time.\u003C\u002Fp>\u003Cul>\u003Cli>How can the _id not be a string when printed inside testCall, the param type is clearly supposed to be string?\u003C\u002Fli>\u003Cli>If it isn’t, why does the linter not yell at me?\u003C\u002Fli>\u003Cli>Maybe it’s a linter bug, but why does it compile then?\u003C\u002Fli>\u003Cli>OK, I made an obvious mistake, according to the schema ObjectId is the correct type, but then again what about all the questions above?\u003C\u002Fli>\u003C\u002Ful>\u003Ch3 id=\"research-and-resolution\">Research and Resolution\u003C\u002Fh3>\u003Cp>I mean once I carefully checked our code, I clearly expected _id to be of type ObjectId as this is what we specified in our schema file.\u003C\u002Fp>\u003Cp>\u003Cem>\"ObjectId probably has some magic implemented to make TypeScript infer it as a string?\" was what we thought when discussing the issue at hand.\u003C\u002Fem>\u003C\u002Fp>\u003Cp>After digging deeper the real issue was revealed.\u003C\u002Fp>\u003Cp>Even though you would expect that Mongoose as a MongoDB ORM would treat _id in a Document to be of type ObjectId, it really is a generic that defaults to \u003Cstrong>any\u003C\u002Fstrong>.\u003C\u002Fp>\u003Cp>What’s up with \u003Cstrong>ANY\u003C\u002Fstrong>?\u003C\u002Fp>\u003Cp>So one thing you need to know is that \u003Cstrong>any\u003C\u002Fstrong> unlike \u003Cstrong>unknown\u003C\u002Fstrong> just disables type-checking for the variable at hand. Using any means you can assign any value to your variable, but also you can assign your variable to any other variable no matter what the target type is — that's why you think you have a string in your hand inside of testCall while it is not the case. Clearly, I had some gaps in my TypeScript knowledge (and you might have too).\u003C\u002Fp>\u003Cp>This thing bugged me so much that I continued researching for a bit and found several problems in our code that revealed an unfortunate chain of disasters, that ultimately led to my situation.\u003C\u002Fp>\u003Ch3 id=\"solution\">Solution\u003C\u002Fh3>\u003Cp>The solution for my problem in this case was very simple. I had to create my Mongoose schema according to the latest docs and use:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-typescript\">export type CatDocument HydratedDocument&lt;Cat&gt;\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>instead of:\u003C\u002Fp>\u003Cpre>\u003Ccode class=\"language-typescript\">export type CatDocument = Cat &amp; Document;\u003C\u002Fcode>\u003C\u002Fpre>\u003Ch3 id=\"learnings\">Learnings\u003C\u002Fh3>\u003Cp>Here are a bunch of issues I found in our project setup while researching the above issue:\u003C\u002Fp>\u003Cul>\u003Cli>We had “noImplicitAny” set to \u003Cstrong>false\u003C\u002Fstrong> in our tsconfig because some colleagues got annoyed with the need to always explicitly cast to any from libraries that do not deliver types.\u003C\u002Fli>\u003Cli>The code we used for our schemas, was originally written by a contractor who used the legacy version from a few years ago: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fnestjs\u002Fdocs.nestjs.com\u002Fpull\u002F2517\" rel=\"nofollow noopener\">https:\u002F\u002Fgithub.com\u002Fnestjs\u002Fdocs.nestjs.com\u002Fpull\u002F2517\u003C\u002Fa>\u003C\u002Fli>\u003Cli>Pay attention to details and dig deep if you find issues that you do not immediately understand, if it doesn’t help to find a better solution it will at least teach you a ton which never hurts.\u003C\u002Fli>\u003C\u002Ful>\u003Ch3 id=\"how-to-do-better-next-time\">How to do better next time\u003C\u002Fh3>\u003Cul>\u003Cli>Use “strict” mode for TypeScript if you want as much help from TypeScript as possible, to not make stupid type-related mistakes\u003C\u002Fli>\u003Cli>Check your code for best practices regularly\u003C\u002Fli>\u003Cli>Try to keep up with changes in third-party libraries that are essential for your own code\u003C\u002Fli>\u003Cli>Pay attention to details\u003C\u002Fli>\u003C\u002Ful>\u003Cp>\u003Cem>Sidenote:\u003Cbr>Mongoose had a similar issue in their HydratedDocument type which is the current default recommendation: \u003C\u002Fem>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FAutomattic\u002Fmongoose\u002Fissues\u002F11085\" rel=\"nofollow noopener\">\u003Cem>https:\u002F\u002Fgithub.com\u002FAutomattic\u002Fmongoose\u002Fissues\u002F11085\u003C\u002Fem>\u003C\u002Fa>\u003Cbr>\u003Cem>I’m still not sure why the Document type doesn’t have a similar implementation or defaults to type _id as ObjectId by default instead of any, but let’s see if their maintainers will answer my question about that.\u003C\u002Fem>\u003C\u002Fp>"]