Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose generic types for WhereInput #6980

Open
stalniy opened this issue May 7, 2021 · 12 comments
Open

Expose generic types for WhereInput #6980

stalniy opened this issue May 7, 2021 · 12 comments
Labels
kind/feature A request for a new feature. team/client Issue for team Client. topic: client types Types in Prisma Client topic: ecosystem topic: extend-client Extending the Prisma Client topic: prisma-client

Comments

@stalniy
Copy link

stalniy commented May 7, 2021

Problem

I'm an author of https://casl.js.org/ and working on integration of prisma with that authorization library. The issue i have is inability to easily get WhereInput type for specific model in a generic way. I need this in order to properly type argument inside my function based on the argument. Primitive example:

// this is currently almost impossible
function can<TModel extends string>(action: string, subject: TModel, conditions: Prisma.WhereInput<TModel>) {
}

Suggested solution

Ideally, I'd like to have something like Prisma.WhereInput<User> or Prisma.WhereInput<'User'>.

Alternatives

There is quite hacky workaround that doesn't work if the client is not generated and requires TS 4.1+:

import * as p from '@prisma/client'

type ModelDelegates = {
  [K in p.Prisma.ModelName]: p.PrismaClient[Uncapitalize<K>]
}

type WhereInput<T> = T extends Model<Record<string, unknown>, p.Prisma.ModelName>
  ? Exclude<Parameters<ModelDelegates[T["kind"]]["findFirst"]>[0], undefined | null>["where"]
  : never;

type Model<T extends Record<string, unknown>, TName extends string> = T & { kind: TName };
type Post = Model<p.Post, 'Post'>;

type PostWhereInput = WhereInput<Post>;
@janpio janpio added kind/feature A request for a new feature. team/client Issue for team Client. topic: prisma-client topic: client types Types in Prisma Client topic: ecosystem labels May 7, 2021
@Jolg42
Copy link
Member

Jolg42 commented May 10, 2021

Note, for info: Prisma Client since 2.14.0 https://github.com/prisma/prisma/releases/tag/2.14.0 requires TypeScript 4.1+

@stalniy
Copy link
Author

stalniy commented May 10, 2021

That's good point! Thanks for letting me know

@stalniy
Copy link
Author

stalniy commented May 10, 2021

I suggest to add something like this to generated types:

export type Models = {
  User: { 
    model: User;
    where: UserWhereInput,
    // other user related types
  }
}

What would make a lot of dynamic type generation cases much easier in future as well. So, I can represent WhereInput<T> as this:

type WhereInput<T extends Prisma.ModelName> = Prisma.Models[T]['where'];

Link to related slack discussion: https://prisma.slack.com/archives/C021N1UPQ0Z/p1620644383003000

@dpetrick dpetrick added the topic: extend-client Extending the Prisma Client label May 19, 2021
@janpio janpio changed the title Feature: expose generic types for WhereInput Expose generic types for WhereInput Jun 20, 2023
@millsp
Copy link
Member

millsp commented Jun 24, 2023

Hey @stalniy, since version 4.16.0, we expose new type utilities to achieve that. Here you go:

function checkWhereInput<T>(model: T, input: Prisma.Args<T, 'findFirstOrThrow'>['where']) {
}

checkWhereInput(prisma.user, {
  /* Auto-completion works */
})

And another variant on type-level only:

type GetWherePayload<T extends Record<any, any>, M extends keyof any> =
  Exclude<Prisma.Args<T[M], 'findFirstOrThrow'>['where'], undefined>

type test = GetWherePayload<typeof prisma, 'user'>

Does that work for you?

@mateussilva92
Copy link

mateussilva92 commented Jun 27, 2023

@millsp
In the example provided what type should T extend so that TS doesn't complain?

I have the following but as soon I have more than one model prisma starts complaining.
I'm not sure if currently there is a solution for this.

type Models = Prisma.TypeMap["meta"]["modelProps"];

function checkWhereInput<T extends PrismaClient[Models]>(model: T, input: Prisma.Args<T, 'findFirstOrThrow'>['where']) {
  model.findFirstOrThrow({where: input});
}

@kdawgwilk
Copy link

@millsp Where are these new type utilities documented?

@millsp
Copy link
Member

millsp commented Jul 4, 2023

@millsp
Copy link
Member

millsp commented Jul 4, 2023

@mateussilva92 This is a general limitation from TypeScript, not Prisma, here's the explanation. With your current code, you are trying to tell TS that you will potentially access any model in a typesafe manner. That seems sound and logic, but the type-system sees otherwise. While your union of models is type-safe, TS will say:

This expression is not callable. Each member of the union type '...' has signatures, but none of those signatures are compatible with each other.

And that is true. Each model can have a findFirst but each findFirst arguments will differ per model. So if TS didn't yell here, it means you could potentially run into a runtime error, thinking it's fine. You could easily pass the input of a model A for an operation of a model B. TypeScript cannot know that you legitimately know what you are doing here. To better understand this, I really recommend this video explanation of what variance is https://www.youtube.com/watch?v=EInunOVRsUU&ab_channel=Prisma

There are ways to get over that though, byt they might not be efficient on the type-system or just not worth it. If your goal is to just have your function to be type-safe on the outside, and it does not matter so much on the inside implementation, I suggest you cast (model as any).findFirstOrThrow({where: input}).

@joao-moonward
Copy link

joao-moonward commented Aug 7, 2023

I want to share some information that I've gathered from previous comments. Hopefully, it will be helpful to someone.

type Models = keyof typeof Prisma.ModelName;

type ArgsType<T extends Models> =
  Prisma.TypeMap['model'][T]['operations']['findMany']['args'];

type WhereType<T extends Models> = NonNullable<ArgsType<T>['where']>;

type AndType<T extends Models> = NonNullable<WhereType<T>['AND']>;

@kdawgwilk
Copy link

@joao-moonward Do you know which min version of prisma you need for that to work?

@joao-moonward
Copy link

joao-moonward commented Aug 9, 2023

@kdawgwilk I used prisma@5.0.0 to run this code. It's possible that it will work with >=4.16.0.

@iki
Copy link

iki commented Apr 11, 2024

@stalniy @joao-moonward @millsp I have a working solution to get model types by name created by inspecting the generated @prisma/client in node_modules/.prisma/client/index.d.ts

import { Prisma, PrismaClient } from '@prisma/client'
import PrismaRuntime from '@prisma/client/runtime/library'

import PrismaTypes = PrismaRuntime.Types

export { PrismaTypes }
export { Prisma, PrismaClient } from '@prisma/client'
export * as PrismaRuntime from '@prisma/client/runtime/library'

export type ModelName = Prisma.ModelName
export type PrismaModelName = ModelName

export const getPrismaModelProp = <N extends PrismaModelName>(name: N) =>
  `${name.charAt(0).toLowerCase()}${name.slice(1)}` as PrismaModelProp<N>

export const getPrismaDelegate = <N extends PrismaModelName>(name: N, prisma: PrismaClient) =>
  prisma[getPrismaModelProp(name)] as PrismaModelDelegate<N> as any // For generic model delegate

// Use `PrismaModel` for any available Prisma model (e.g. as a method parameter or generic type parameter)
// Use `PrismaModel<"YourModelName">` for specific Prisma model
export type PrismaModel<N extends ModelName = ModelName> = PrismaTypes.Result.DefaultSelection<PrismaModelPayload<N>>
export type PrismaModelProp<N extends ModelName = ModelName> = Uncapitalize<N>
export type PrismaModelType<N extends ModelName = ModelName> = Prisma.TypeMap['model'][N]
export type PrismaModelPayload<N extends ModelName = ModelName> = PrismaModelType<N>['payload']
export type PrismaModelDelegate<N extends ModelName = ModelName> = PrismaClient[PrismaModelProp<N>]

export type PrismaModels = { [N in Prisma.ModelName]: PrismaModel<N> }
export type PrismaModelProps = { [N in Prisma.ModelName]: PrismaModelProp<N> }
export type PrismaModelTypes = { [N in Prisma.ModelName]: PrismaModelType<N> }
export type PrismaModelPayloads = { [N in Prisma.ModelName]: PrismaModelPayload<N> }
export type PrismaModelDelegates = { [N in Prisma.ModelName]: PrismaModelDelegate<N> }

export type FindManyArgs<N extends ModelName> = PrismaModelType<N>['operations']['findMany']['args']
export type FindUniqueArgs<N extends ModelName> = PrismaModelType<N>['operations']['findUnique']['args']
export type WhereInput<N extends ModelName = ModelName> = NonNullable<FindManyArgs<N>['where']>
export type WhereAnd<N extends ModelName = ModelName> = NonNullable<WhereInput<N>['AND']>
export type WhereOr<N extends ModelName = ModelName> = NonNullable<WhereInput<N>['OR']>
export type WhereUniqueInput<N extends ModelName = ModelName> = NonNullable<FindUniqueArgs<N>['where']>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature A request for a new feature. team/client Issue for team Client. topic: client types Types in Prisma Client topic: ecosystem topic: extend-client Extending the Prisma Client topic: prisma-client
Projects
None yet
Development

No branches or pull requests

10 participants