Typescript types and when not to trust them (1 of n)

Typescript disappears at runtime. Hence, types disappear at runtime too. Do not fully trust the types. Example here: Middlewares.
August 07, 2023

Let's say that you are building a nodeJS application through Typescript. In your core, you have everything setup with types and they don't complain. You build your application and it is succesfully completed. Let's check an example when this could not work.

ORM's and middlewaring

If you're using an ORM to manage your database connections and transactions, probably you'll have some middlewares to synthetize repetitive processes (logging, soft deletion enabling,...). EG, Prisma allows to implement middlewares per transaction and modify the transaction on the fly. Perfect if you want to use a soft-delete feature. Let's define a Prisma Model ready for soft-deletion.

model Post {
  id      Int     @id @default(autoincrement())
  title   String
  content String?
  user    User?   @relation(fields: \[userId], references: \[id])
  userId  Int?
  tags    Tag\[]
  views   Int     @default(0)
  deleted Boolean @default(false)
}

Now, we could use a middleware to intercept the transactions related to the Post model and adapt them to the soft-delete philosophy (now, to retrieve or modify a Post, the query should also imply that the deleted property is set to false. Also, the DELETE transactions should be converted to UPDATE ones).

This snippet modifies the Post model DELETE queries to update ones. Cool:

   if (params.model == 'Post') {
      if (params.action == 'delete') {
        // Delete queries
        // Change action to an update
        params.action = 'update'
        params.args['data'] = { deleted: true }
      }
      if (params.action == 'deleteMany') {
        // Delete many queries
        params.action = 'updateMany'
        if (params.args.data != undefined) {
          params.args.data['deleted'] = true
        } else {
          params.args['data'] = { deleted: true }
        }
      }
    }

The same should be done with the SELECT and UPDATE transactions, in order to retrieve only if the selected field is set to false. And the original problem comes with the UPDATE transaction.

prisma.$use(async (params, next) => {

  if (params.model == 'Post') {

    if (params.action == 'update') {

    // Change to updateMany - you cannot filter
    // by anything except ID / unique with findUnique

      params.action = 'updateMany'

      // Add 'deleted' filter
      // ID filter maintained
      params.args.where['deleted'] = false

  }

if (params.action == 'updateMany') {

  if (params.args.where != undefined) {

    params.args.where\['deleted'] = false

    } else {

    params.args\['where'] = { deleted: false }

    }

  }

}

 return next(params)

})

In order to update a model allowed to be soft-deleted, we need to use the updateMany action and setup the delete=false clause . But, when we do that, we're changing the original update action. (https://www.prisma.io/docs/concepts/components/prisma-client/middleware/soft-delete-middleware#option-2-use-middleware-to-determine-the-behavior-of-readupdate-queries-for-deleted-records)

Prisma's update action returns the updated model. Prisma's updateMany action returns a { count: n } object (without exported typing). Type mismatching here...

We're using here a middleware that converts one action into another. But the PrismaClient won't be aware of that until runtime. So, you'll probably use something like this in your code:

const updatePost:  Post = await prisma.post.update({

where: {

id: postsCreated\[1].id,

},

data: {

title: 'This is an updated title (update)',

},

})

and the TS compiler won't fail. You're updating a post, it returns a Post model. Nothing to worry. But at runtime, the middleware will switch the actions and your Post will be a {count: n} object, turning your code at runtime to something like this:

const updatePost:  {count: n} = await prisma.post.update({

  where: {

    id: postsCreated[1].id,

    },

  data: {

  title: 'This is an updated title (update)',

  },

})

If you're using the updatePost object somewhere else, you'll have type mismatches between runtime and compilation too...

Although, nothing to worry to much. You can make the conversion on the fly:

const updatePost:  {count: number} = await prisma.post.update(
...) as unknown as {count: number}

But, well... You've been tricked by the type system at this point. You have to force a type there to do not have crashes at runtime. It means that you trusted the type system and it failed...

More examples to come.