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.16k stars 1.53k forks source link

Client Extensions destroys types using Prisma with javascript #20128

Open adamjkb opened 1 year ago

adamjkb commented 1 year ago

Bug description

When using Prisma in a .js file types are erased once the Client is extended. This is happening whether the extension is mounted directly or created with defineExtension.

How to reproduce

  1. Go to https://github.com/prisma/prisma-client-extensions/tree/main/static-methods
  2. Change script.ts to script.js (and remove TS syntax)
  3. See types being erased from the prisma variable

Expected behavior

Types to be inferred just as they would when Prisma is used in Typescript file (.ts)

Prisma information

generator client {
  provider        = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model User {
  id       String    @id @default(cuid())
  email    String
  password Password?
}
// index.js
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient().$extends({
  model: {
    user: {
      async findSomething(arg) {
        return null
      }
    }
  }
});

const x = await prisma.user.findSomething()
// type is `any` ^^^

Environment & setup

Prisma Version

prisma                  : 4.16.2
@prisma/client          : 4.16.2
Current platform        : darwin
Query Engine (Node-API) : libquery-engine 4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 (at node_modules/@prisma/engines/libquery_engine-darwin.dylib.node)
Migration Engine        : migration-engine-cli 4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 (at node_modules/@prisma/engines/migration-engine-darwin)
Format Wasm             : @prisma/prisma-fmt-wasm 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
Default Engines Hash    : 4bc8b6e1b66cb932731fb1bdbbc550d1e010de81
Studio                  : 0.484.0
Jolg42 commented 1 year ago

@adamjkb so I did the following:

git clone https://github.com/prisma/prisma-client-extensions
cd prisma-client-extensions/static-methods
npm i
// created the .js file

And indeed I get any

Screenshot 2023-07-10 at 11 16 35

But restarting the TS Server fixes the issue for me

Screenshot 2023-07-10 at 11 16 52 Screenshot 2023-07-10 at 11 17 06

Does that work for you too?

adamjkb commented 1 year ago

thanks for getting back to me @Jolg42. Indeed, on my end too, it gets that type however that is as far as they go. All client types are now gone. So, user and all the other built-in types are gone. ($transaction etc...).

And seemingly the type is the same across both versions but I believe it is hidden so it lies further up. So the difference is in the type of the $extends call gets back.

TS version:

(property) PrismaClient<Prisma.PrismaClientOptions, never, Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined, DefaultArgs>.$extends: ExtendsHook
<{
    $allModels?: unknown;
    user?: unknown;
    password?: unknown;
}, unknown, {
    user: unknown;
}, {
    user: {
        findSomething(arg: any): Promise<null>;
    };
}, {
    ...;
}, {
    ...;
}, unknown, InternalArgs<...>, {
    ...;
}>(extension: ((client: DynamicClientExtensionThis<...>) => {
    ...;
}) | {
    ...;
}) => DynamicClientExtensionThis<...>

JS version:

property) PrismaClient<Prisma.PrismaClientOptions, never, Prisma.RejectOnNotFound | Prisma.RejectPerOperation | undefined, DefaultArgs>.$extends: ExtendsHook
<{
    $allModels?: unknown;
    user?: unknown;
    password?: unknown;
}, any, {
    user: unknown;
}, {
    user: {
        findSomething(arg: any): Promise<null>;
    };
}, {
    $allModels?: unknown;
    user?: unknown;
    password?: unknown;
    $allOperations?: unknown;
    $executeRawUnsafe?: unknown;
    $executeRaw?: unknown;
    $queryRawUnsafe?: unknown;
    $queryRaw?: unknown;
}, {
    ...;
}, any, InternalArgs<...>, {
    ...;
}>(extension: ((client: DynamicClientExtensionThis<...>) => {
    ...;
}) | {
    ...;
}) => DynamicClientExtensionThis<...>

It's hard to see and it is truncated but note the appearance of anys on the line: }, any, InternalArgs<...>, { and further up as well }, any, {. These will essentially merge the client's functions to type any if it were unknown it were get ignored and others would take precedent but any is "known" and overrides this.

I mentioned this on the PR but basically it has to do with how TS type server behaves in a JS environment: Every generics is initialized as any:

The any fallback is one of the difference how JS is different from TS: https://github.com/microsoft/TypeScript/issues/30009#issuecomment-469385244

So, if we were to explicitly initialize these types to unknown (and others to their default) this problem goes away.

You can actually hot patch my fixes from the PR in your node_modules file to see this behavior resolved by it since it doesn't affect any of the generated types.

I'm open to other solutions though, I can fully see my PR solving the problem at the wrong place and it's possible that this would be solved by further up the system.

adamjkb commented 1 year ago

Can confirm the bug still persists after version 5.0.0.

aqrln commented 1 year ago

Reproducible with latest Prisma from main branch.

Type of prisma:

Image

Type of prisma.user:

Image

Methods like prisma.$transaction are also any, and there are no useful autocompletions after typing prisma..

mordechaim commented 6 months ago

I reintroduce types using JSDoc as:

/** @type {import('@prisma/client').PrismaClient} */
const prisma = new PrismaClient().$extends({
    // extension definition
})

But I lose typings on the extension, only getting the core client types.