prisma / prisma

Next-generation ORM for Node.js & TypeScript | PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, MongoDB and CockroachDB
https://www.prisma.io
Apache License 2.0
39.47k stars 1.54k forks source link

Prisma throws type mismatch error using Decimal type from from 'decimal.js' package #6021

Closed JonDum closed 2 years ago

JonDum commented 3 years ago

Bug description

Prisma generates a namespace for Decimal.js that does not match the type from the import of Decimal.js from npm. This makes it annoying to use when third party code gives you "decimal.js Decimal's" and not "prisma Decimal's".

Type 'Decimal[]' is not assignable to type 'string | number | Decimal | Decimal[] | TestCreatefooInput | number[] | string[]'

How to reproduce


model Test {
  id  Int       @id @default(autoincrement())
  foo Decimal[]
}
iimport { PrismaClient } from '@prisma/client'

import {Decimal as PrismaDecimal} from '@prisma/client/runtime'
import {Decimal as DecimalActual} from 'Decimal.js'

const prisma = new PrismaClient()

const actualDecimals = [new DecimalActual(1), new DecimalActual(2), new DecimalActual(3)]
const prismaDecimals = [new PrismaDecimal(1), new PrismaDecimal(2), new PrismaDecimal(3)]

prisma.test.create({
    data: {
        foo: actualDecimals
      // ^ Type 'Decimal[]' is not assignable to type 'string | number | Decimal | Decimal[] | TestCreatefooInput | number[] | string[]'
    }
})

prisma.test.create({
    data: {
        foo: prismaDecimals
    }
})

Expected behavior

Doesn't complain about mismatched types

Prisma information

{
  "@prisma/client": "^2.18.0",
  "decimal.js": "^10.2.1"
}

Environment & setup

Environment variables loaded from .env
prisma               : 2.18.0
@prisma/client       : 2.18.0
Current platform     : darwin
Query Engine         : query-engine da6fafb57b24e0b61ca20960c64e2d41f9e8cff1 (at ../../.config/yarn/global/node_modules/@prisma/engines/query-engine-darwin)
Migration Engine     : migration-engine-cli da6fafb57b24e0b61ca20960c64e2d41f9e8cff1 (at ../../.config/yarn/global/node_modules/@prisma/engines/migration-engine-darwin)
Introspection Engine : introspection-core da6fafb57b24e0b61ca20960c64e2d41f9e8cff1 (at ../../.config/yarn/global/node_modules/@prisma/engines/introspection-engine-darwin)
Format Binary        : prisma-fmt da6fafb57b24e0b61ca20960c64e2d41f9e8cff1 (at ../../.config/yarn/global/node_modules/@prisma/engines/prisma-fmt-darwin)
Studio               : 0.356.0
jreilly-lukava commented 3 years ago

Preface: As you might note below, I'm using a custom output folder for my client, not generating it into @prisma/client.

Because these two types are not the same, I attempted to use Prisma.Decimal on my angular frontend to maintain strong typing. However when I do that and attempt to server my angular application I'm met with a slew of message similar to

Error: ./libs/stats-models/.prisma/client/runtime/index.js
Module not found: Error: Can't resolve 'zlib' in 'D:\Monorepo\libs\stats-models\.prisma\client\runtime'
pantharshit00 commented 3 years ago

This is still reproducible in 2.23. I added the candidate label to this.

matthewmueller commented 3 years ago

Does having the same Decimal.js version still have this problem? Prisma is using "decimal.js": "10.2.1"

arenddeboer commented 3 years ago

Does having the same Decimal.js version still have this problem? Prisma is using "decimal.js": "10.2.1"

Yes, same problem with "10.2.1"`

The problem seems to stem from the name property as declared in the Decimal version of Prisma.

private readonly name: string;

Type 'Decimal' is not assignable to type 'string | number | Decimal'.
  Property 'name' is missing in type 'import("/project/node_modules/decimal.js/decimal").default' but required in type 'import("/project/node_modules/@prisma/client/runtime/index").Decimal'.ts(2322)
index.d.ts(832, 20): 'name' is declared here.
SimonCockx commented 2 years ago

Not solved in 3.15.2. I get a type error when doing this:

new PrismaClient().test.findUnique({ where: { id: 0 } }).then((t) => {
    const foo: Decimal = t!.foo;
    //    ^^^
});

Type 'import("[...]/node_modules/@prisma/client/runtime/index").Decimal' is not assignable to type 'import("[...]/node_modules/decimal.js/decimal").default'. Types have separate declarations of a private property 'toStringTag'.

given a model

model Test {
  id  Int       @id @default(autoincrement())
  foo Decimal
}
SevInf commented 2 years ago

@SimonCockx I believe it's not the same problem and the type error is technically correct. What we did to fix original issue is that we expanded what decimal-like things we accept as input. We can do this since there is very little we need from Decimal.js for Prisma to function.

In your case, you are trying to assign Prisma's output to the external Decimal variable. However, the output did not change: even though Prisma can now accept wide range of Decimal instances as an input, it still can return only it's own bundled version as an output. We can't say that Prisma.Decimal output will be compatible with Decimal coming from external library since it simply won't be true.

The safest way for you to ensure that you always get your kind of decimal no matter what changes in Prisma bundled version or external library is to go through manual conversion:

const foo: Decimal = new Decimal(String(t!.foo));
SimonCockx commented 2 years ago

@SevInf Okay. I believed that the goal of this issue was complete compatibility with decimal.js, but I guess that's not true then. Thanks for the explanation and the workaround.

alberto467 commented 2 years ago

This is very dirty stuff, but it's what my project needed.

import { Prisma } from '@prisma/client'
import Decimal from 'decimal.js'

type ReplaceDecimal<T> = {
  [K in keyof T]: T[K] extends Prisma.Decimal
    ? Decimal
    : T[K] extends Record<string, unknown>
    ? ReplaceDecimal<T[K]>
    : T[K] extends Record<string, unknown>[]
    ? ReplaceDecimal<T[K][0]>[]
    : T[K]
}

// I'm using trpc and overriding it's type inference to wrap all values returned with ReplaceDecimal,
// but you could wrap your types with ReplaceDecimal using other solutions too
declare module '@trpc/server' {
  type inferProcedureOutput<TProcedure extends Procedure<any, any, any, any, any, any, any>> = ReplaceDecimal<
    inferAsyncReturnType<TProcedure['call']>
  >
}

Now i can finally do stuff like:

import { Product } from '@prisma/client'
import Decimal from 'decimal.js'

const products: Product[] // Types from prisma, originally containing Prisma.Decimal
Decimal.sum(...products.map(p => p.price)) // Decimal from decimal.js

Even though Prisma's decimal is not fully compatible with decimal.js, i can be sure of the compatibility since this is frontend code and the decimals are serialized by superjson using decimal.js. I wouldn't use this already hacky approach if your decimals are not going through proper serialization with decimal.js.

I would love it if Prisma started exporting it's decimal types straight from decimal.js, while it would maybe break support for other decimal libraries, one could argue there already is no compatibility for any decimal libraries for use cases such as mine, since Prisma's own decimal can't be used on the browser and dirty hacks like this or tedious type castings are needed. It would really smoothen the experience considering how popular backend-to-frontend types are.

SevInf commented 2 years ago

can't be used on the browser

That's not true, prisma generates a special bundle for browsers which allows to use bundled Decimal and generated enums on the frontend. Take a look at index-browser.js in generated directory. Your bundler also should be able to pick up the correct version when building for browsers.

alberto467 commented 2 years ago

I didn't think it was possible! I was getting an error from the chalk dependency on the frontend, turns out i was importing Decimal from @prisma/client/runtime and that doesn't load the browser version automatically, but using @prisma/client/runtime/index-browser works great and importing from @prisma/client picks up the right version automagically. This fits perfectly my use case and i'll be refactoring my code to use prisma's decimals only. Thanks a lot.

SevInf commented 2 years ago

Just to be clear, you don't need to import it from the runtime module or specify index-browser.js explicitly. This should work in both browser and node:

import { Prisma } from '@prisma/client`

new Prisma.Decimal(123)

and the frontend bundler should be able to automatically resolve this import to index-browser.js for you.