Skip to content

Conversation

FGoessler
Copy link
Contributor

@FGoessler FGoessler commented Jul 30, 2025

This PR improves the complex type resolution of the PrismaClient type in case ClientOptions are provided. e.g.:

const client = new PrismaClient( /* Client Options */ { datasourceUrl: "..." })

Why is providing client options causing type performance issues?

The PrismaClient interface changes dynamically based on the provided options. There are two options relevant for this behavior: log and omit configuration.

log configuration changes the signature of the $on method of the client. This provides type safety so one can only observe log messages/events that are actually activated through the client options.

const client = new PrismaClient({
  log: [
    { level: 'query', emit: 'event' },
    { level: 'error', emit: 'stdout' },
  ],
})
// ok:
client.$on('query', (event) => { ... })
// type error:
client.$on('info', (event) => { ... })

omit configuration changes what fields on models are available by default. This has wide reaching type implications across the PrismaClient instance and also deep into potential client extensions.

const client = new PrismaClient({
  omit: {
    user: {
      name: true,
    },
  },
})

Due to those customizations a PrismaClient is not equal to any other PrismaClient. To check whether one instance is assignable to another (e.g. when passing a PrismaClient into a helper method) TypeScript has to perform some very deep checks that run through deep and complex generics. If no client options are provided TypeScript can usually "short circuit" and is very efficient - once client options are provided way more work has to be done.

Existing workarounds

We have a known workaround to use the typeof operator to avoid redundant type resolution. Through this the user can give a specific PrismaClient instance an "named alias" and use this in their code. TypeScript then does not have to repeatedly check full type assignability but can rely on the equality of the named alias.

const client = new PrismaClient({ ... })

type MyPrismaClient = typeof client

const passClientAround = (prisma: MyPrismaClient) => {
  ...
}

Approach of this PR

Currently any variant of client options triggers the full complexity type check and prevents TypeScript from taking the "short circuit" route. Even if the provided client options have no influence on the actual type interface when they specify no omit or log configuration.

The idea is now to modify the type of the PrismaClient such that the type performance penalty is only paid if the complex checks for omit and log are actually necessary. To achieve this PrismaClient itself was changed to not depend on a generic ClientOptions but on separate LogOpts and OmitOpts. These are extracted from the ClientOptions in the constructor.

// Before
export interface PrismaClientConstructor {
  new <
    ClientOptions extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
    const U = LogOptions<ClientOptions>,
    ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
  >(options?: Prisma.Subset<ClientOptions, Prisma.PrismaClientOptions>): PrismaClient<ClientOptions, U, ExtArgs>
}

export interface PrismaClient<
  ClientOptions extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
  U = LogOptions<ClientOptions>,
  ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
> {
  ...
}

// After
export interface PrismaClientConstructor {
  new <
    Options extends Prisma.PrismaClientOptions,
    LogOpts extends LogOptions<Options>,
    OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
    ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
  >(options?: Options): PrismaClient<LogOpts, OmitOpts, ExtArgs>
}

export interface PrismaClient<
  in LogOpts extends Prisma.LogLevel = never,
  in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'],
  in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
> {
  ...
} 

Now two type instances of PrismaClient are only different if their LogOpts or OmitOpts are actually different. For most cases the type check is now way simpler and can go the "short circuit" routes. As the dependencies are now smaller for LogOpts, just providing log configuration causes way less type check overhead than before. Only with omit configuration provided one still pays a high complexity price (although that also improved significantly!).

This can be an acceptable tradeoff as omit client options is a more rarely used feature and we can extend the documentation to encourage users to use the typeof workaround when using omit client options.

You can see the magnitude of improvements in the updated type benchmark instantiation counts. In some cases for large schemas we went from about 13 million to below 1k instantiations!

Change in generic type signature

Warning

This change comes with one caveat: As the PrismaClient generic arguments type signature had to change its a technically breaking change.

Which should be okay as this generator is still in preview and I would not expect many users to explicitly specify generic type params on their PrismaClient type. Even if so it is easy to work around and resolve. Ideally by not specifying the generic type arguments but letting them get inferred, using the typeof operator, or in worst case updating the generic arguments as shown below.

Log Level Case:

// Before
let prisma: PrismaClient<{ log: [{ emit: 'event'; level: 'query' }] }>
// or:
let prisma: PrismaClient<{ log: ['query'] }>
// After
let prisma: PrismaClient<'query'>

Omit Case:

// Before
let prisma: PrismaClient<never, omit: { user: { password: true } }>
// After
let prisma: PrismaClient<never, { user: { password: true } }>

Might be bigger issue for some library developers though? 🤔

It is also an issue in our test infrastructure as we have a few client tests that need explicitly specified LogOpts. The same tests run on the new and the old generator though. So with this change we had to introduce complexity to change the test code based on what generator it is supposed to run with. For the globalOmit test in particular I duplicated the test to have one test run on the new generator only and the other on the old generator only.

@FGoessler FGoessler changed the title fix/ORM 850 type variance annotations fix: ORM 850 type variance annotations Jul 30, 2025
Copy link
Contributor

github-actions bot commented Jul 30, 2025

size-limit report 📦

Path Size
packages/client/runtime/library.js 403.62 KB (0%)
packages/client/runtime/library.d.ts 81 B (0%)
packages/client/runtime/binary.js 1.27 MB (0%)
packages/client/runtime/binary.d.ts 26 B (0%)
packages/client/runtime/edge.js 339.45 KB (-0.01% 🔽)
packages/client/runtime/edge-esm.js 338.1 KB (0%)
packages/client/runtime/wasm-engine-edge.js 283.76 KB (0%)
packages/client/runtime/index-browser.js 67.74 KB (0%)
packages/client/runtime/index-browser.d.ts 89 B (0%)
packages/cli/build/index.js 2.7 MB (+0.01% 🔺)
packages/client/prisma-client-0.0.0.tgz 33.68 MB (+0.01% 🔺)
packages/cli/prisma-0.0.0.tgz 16.61 MB (+0.01% 🔺)
packages/bundle-size/da-workers-libsql/output.tgz 945.28 KB (0%)
packages/bundle-size/da-workers-neon/output.tgz 1021.87 KB (0%)
packages/bundle-size/da-workers-pg/output.tgz 1 MB (0%)
packages/bundle-size/da-workers-planetscale/output.tgz 966.15 KB (0%)
packages/bundle-size/da-workers-d1/output.tgz 929.53 KB (0%)

@FGoessler FGoessler force-pushed the fix/ORM-850-type-variance-annotations branch from fbd2d00 to c7ae293 Compare July 31, 2025 11:58
@FGoessler FGoessler added this to the 6.14.0 milestone Jul 31, 2025
@FGoessler FGoessler changed the title fix: ORM 850 type variance annotations fix: ORM 850 improve client options type performance Jul 31, 2025
@FGoessler FGoessler marked this pull request as ready for review August 6, 2025 06:35
@FGoessler FGoessler requested a review from a team August 6, 2025 06:35
@FGoessler FGoessler changed the title fix: ORM 850 improve client options type performance fix: ORM-850 improve client options type performance Aug 6, 2025
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Aug 11, 2025
@aqrln aqrln merged commit 21765b0 into main Aug 12, 2025
396 checks passed
@aqrln aqrln deleted the fix/ORM-850-type-variance-annotations branch August 12, 2025 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lgtm This PR has been approved by a maintainer
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants