prisma: Specifying the "select" param in a query using a variable with a defined type breaks types

Bug description

After upgrading Prisma from 2.2 to 2.5, I’m now running into an issue where, when I try to provide the select parameter using a typed variable rather than inline code in a query, the result of the query no longer has any type information associated with it.

For example, I have a User type that has a number of properties, like id, createdAt, etc.

If I attempt to do a query with the select being defined inline, types are present on the result as expected:

image

If I now define my selection property using a typed variable, however (with the type corresponding to to the type expected by the Prisma for the query), and the pass that in instead, the result no longer has any properties, being typed as an empty object instead:

image

Oddly, if I remove the typing on the select variable, type information returns to the result (but I no longer have type suggestions when composing the select variable)

image

How to reproduce

Take any query, extract the select property into a typed variable.

Expected behavior

I expect the result of the query to have type information that corresponds to what the select variable is set to.

Prisma information

This appears to apply to any and all schemas, types, and queries.

Environment & setup

  • OS: Windows
  • Database: PostgreSQL
  • Node.js version: 10.16
  • Prisma version: 2.5.1

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 5
  • Comments: 31 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Thanks for all the comments. We are talking about two distinct problems here.

1. Problem: How do I separately construct the select or include argument and not just inline?

2. Problem: How do I create a wrapper function around any Prisma query, like findMany that is typesafe?

These problems require separate solutions. In the following, I’ll show how they’re solvable today and also mention a potential utility type that we might want to generate in the client to save you some typing.

1. Problem: Dynamically construct select or include

This is, what @artemzakharov and @vh13294 requested. This can be achieved with a little helper function, as Harshit already noted. Here called makeUserSelect:

import { PrismaClient, Prisma } from './@prisma/client'

function makeUserSelect<T extends Prisma.UserSelect>(
  select: Prisma.Subset<T, Prisma.UserSelect>,
): T {
  return select
}

const select = makeUserSelect({ id: true })

const user = await prisma.user.findFirst({
  where: {
    id: 'id',
  },
  select,
})

const id = user?.id

The same for include:

function makeUserInclude<T extends Prisma.UserInclude>(
  include: Prisma.Subset<T, Prisma.UserInclude>,
): T {
  return include
}

const include = makeUserInclude({
  likes: true
})

const user = await prisma.user.findFirst({
  where: {
    id: 'id',
  },
  include,
})

const likes = user?.likes

We can even do this for the whole args, here an example for findMany:

function makeUserFindManyArgs<T extends Prisma.UserFindManyArgs>(
  include: Prisma.Subset<T, Prisma.UserFindManyArgs>,
): T {
  return include
}

const args = makeUserFindManyArgs({
  include: {
    likes: true
  }
})

const users = await prisma.user.findMany(args)

const likes = users[0].likes

With our current knowledge, we need to create one function per type we want to “make”. We will later investigate if we could simplify that.

2. Problem: How do I create a wrapper function around any Prisma query, like findMany that is typesafe?

This is, what @MichalLytek and @Sytten talk about. This can be done without any helper type. We can make this happen both for select and include, but not the whole args:

Just passing in select, as requested by @MichalLytek

function findNewPrismaUsersInNearbySelect<S extends Prisma.UserSelect>(
  select: Prisma.Subset<S, Prisma.UserSelect>,
) {
  return prisma.user.findMany<{ select: S }>({ select, where: {} })
}

const users = await findNewPrismaUsersInNerbySelect({ id: true })
users[0].id

The same for include:

function findNewPrismaUsersInNearbyInclude<S extends Prisma.UserInclude>(
  include: Prisma.Subset<S, Prisma.UserInclude>,
) {
  return prisma.user.findMany<{ include: S }>({ include })
}
const users = await findNewPrismaUsersInNearbyInclude({ posts: true })
users[0].posts

We can’t do this for the whole args here:

function findNewPrismaUsersInNearby<T extends Prisma.UserFindManyArgs>(
  args: Prisma.Subset<T, Prisma.UserFindManyArgs>,
) {
  return prisma.user.findMany({...args, where: {}})
}

const users = await findNewPrismaUsersInNearby({ select: { id: true } })
// types will be incorrect

But that also hasn’t been asked 🙂

Does this solve your problem? Please let us know 🙏

Next steps for us at Prisma:

  • Document this
  • Find out, which make... types we want to expose

@pantharshit00 I still have a problem with the select generic argument. It works well when I define it as required:

function getUserById<TSelect extends Prisma.UserSelect>(id: number, select: Prisma.Subset<TSelect, Prisma.UserSelect>) {
  return this.readQuery.findUnique({
    where: { id },
    select,
    rejectOnNotFound: true,
  });
}

However, I would like to maintain the optional select behavior, so without providing that argument it returns full entity:

function getUserById<TSelect extends Prisma.UserSelect>(id: number, select?: Prisma.Subset<TSelect, Prisma.UserSelect>) {
  return this.readQuery.findUnique({
    where: { id },
    select,
    rejectOnNotFound: true,
  });
}

The problem is that when I add ?, it now always return {} as the return type of the data. The mentioned workaround { select: S } & Omit<Prisma.UserFindFirstArgs, 'select' | 'include'> however makes this function always return full User entity, not the subset, ignoring the provided select argument.

How can I type this? Maybe we can use conditional type and check if it’s defined, then have return type based on Prisma.UserGetPayload? 🤔

EDIT: Simple solutions are the best 😅

  function getUserById(id: number): Promise<User>;
  function getUserById<TSelect extends Prisma.UserSelect>(
    id: number,
    select: Prisma.Subset<TSelect, Prisma.UserSelect>,
  ): Promise<Prisma.UserGetPayload<{ select: TSelect }>>;
  function getUserById<TSelect extends Prisma.UserSelect>(
    id: number,
    select?: Prisma.Subset<TSelect, Prisma.UserSelect>,
  ): Promise<Prisma.UserGetPayload<{ select: TSelect }>> { ... }

Why is this issue closed? Its so hard to work with Prisma when you are always fighting against its type system with unpredictable results.

Definitely, that’s the workaround the validator uses:

declare function validator<V>(): <S>(select: Exact<Narrow<S, V>, V>) => S;

I’m working on a codebase that relies (too) much on this - it’s heavy for my cognitive load. But there is a potential PR candidate that should solve this problem in the near (?) future: https://github.com/microsoft/TypeScript/pull/26349

Though it’s not certain that this will solve our use case here. It will depend on whether we will be allowed to use the _ as a default parameter or not. A few people already have asked for this. In a perfect world, we would be able to do:

declare function validator<V, S = _>(select: Exact<Narrow<S, V>, V>) => S;

Time will tell, if this is not supported, then we will fallback to this issue https://github.com/microsoft/TypeScript/issues/10571

The TypeScript team has added this on their roadmap, but no date is yet set. This is one of the most exciting (missing) features for TypeScript today, IMO.

We just got help from a TypeScript god - Pierre-Antoine Mills @millsp, the author of ts-toolbelt (without his work, we wouldn’t have group by today). He has some ideas: https://github.com/timsuchanek/make-type/blob/master/draft.ts Unfortunately, TypeScript has an annoying limitation, that you can’t “skip” type parameters. Therefore, it looks like we have to generate the createUserSelect and createUserInclude API for all types.

And @millsp thanks a lot for the help there! I just merged your new types and they work like a charm!

@timsuchanek What is the recommended way to create business logic wrappers around Prisma Client?

Translating function params to prisma where object is easy but I can’t simply make the returned type typesafe while allowing consumers to request only selected fields (or relations):

function findNewPrismaUsersInNearby(select?: Prisma.UserSelect) {
  return prisma.user.findMany({
    where: {
      firstName: {
        contains: 'Prisma',
      },
      country: {
        in: ['Germany', 'Poland'],
      },
      createdAt: {
        gte: new Date('2020-12-20'),
      },
    },
    select,
  });
}

This narrows the returned object type to {}:

function findNewPrismaUsersInNearby(select?: Prisma.UserSelect | undefined): Promise<{}[]>

return prisma.user.findMany<{ select: S }>({ select, where: {} })

@timsuchanek this snippets gives a TS error 😛

Argument of type '{ select: Prisma.Subset<S, Prisma.UserSelect>; where: {}; }' is not assignable to parameter of type '{ select: S; }'.
  Object literal may only specify known properties, and 'where' does not exist in type '{ select: S; }'.

It has to be explicitly .findMany<{ select: S, where: Prisma.UserWhereInput}>(...) which adds more boilerplate if you want to add more params like rejectOnNotFound: true 😞

Second case, I can’t make this approach work if I want the select to be optional like with direct prisma client usage. If I make this parameter optional, the return type becomes {} 😕

Thanks @artemzakharov for reporting. As @pantharshit00 correctly noted, this is intended to not work anymore.

The reason is, that if you typecast it to the Photon.UserSelect type, that this is not type safe anymore. Simple example:

type UserSelect {
  id: boolean
  name: boolean
}

const select: UserSelect = { id: true }

const result = await prisma.user.findMany({ select })

In this example result will now also include name in the type, as the type of select is UserSelect which also has the name, even though the const select doesn’t have. In other words, Prisma Client is not looking into the values but just the types for the ts type validation / inference.

Exactly for this situation that you described we introduced more strict type checks, that make sure, that you don’t just import UserSelect from @prisma/client and type cast the client, as that would break the type safety.

The only solution we have within TypeScript for this, is, to remove the type cast of your userSelection variable and let TypeScript validate things later in the findOne call. I know that’s not the 100% optimal solution, but for now, this is all we can do here.

Something we could definitely improve - we should in the types detect this case and instead of having the empty type actually generate a nice type-level error message, that describes the problem and the solution.

Unfortunately, TypeScript has an annoying limitation, that you can’t “skip” type parameters.

I always use this trick:

function wrapper<TExplicitType>() {
  return function <TImplictType>(t: TImplictType): TImplictType & TExplicitType {
    // ...
  }
}

const foobar = wrapper<Foo>()(bar);

Errors did not show up when only bad fields were passed:

  userValidator({ asd: true });

This came from a mistake I made while writing Exact. It is a type utility that forces a type to comply by another one - with not more and not less properties. I was trying to improve intellisense errors by doing a type cast… which caused an untested case to fail. So I rewrote it, and all errors are caught properly (with nice intellisense errors):

type Exact<A, W = unknown> = 
W extends unknown ? A extends Narrowable ? Cast<A, W> : Cast<
{[K in keyof A]: K extends keyof W ? Exact<A[K], W[K]> : never},
{[K in keyof W]: K extends keyof A ? Exact<A[K], W[K]> : W[K]}>
: never;

type Narrowable = string | number | boolean | bigint;

type Cast<A, B> = A extends B ? A : B;

Exact takes any object A and makes it comply by W. If A has a prop that is in not in W, we mark it as never. If A[K] complies by W[K] we return A[K], otherwise we return W[K] and this will give us better intellisense errors in case of conflicts.

This is a more mature Exact type utility than the ones proposed in https://github.com/microsoft/TypeScript/issues/12936#issuecomment-368244671 since it is able to:

  1. Give explicit errors about types at any depth
  2. Not to fail on mixed unions / distribute well
  3. Fail when it should / avoid to use intersect
  4. Narrow the user input given that it is correct

Community-developed Exact utilities all fail at least one of the points (1, 2, 3) found just above (where points 1, 2, and 3 are the most important). But in the end it’s no surprise because emulating features for the compiler/checker is certainly not an easy task.

(If you’re interested to see the comparison and failures here or at https://github.com/millsp/make-type/blob/master/draft.ts#L42-L79)

@TiE23 Are you able to get type hints as well when calling the function? The return type works as expected but auto completion does not seem to be working.

@timsuchanek this snippets gives a TS error 😛

Oops, I think I only tested it without the where on my machine 🙈 I see this as a possible solution to make it more general:

function findNewPrismaUsersInNearbySelect<S extends Prisma.UserSelect>(
  select: Prisma.Subset<S, Prisma.UserSelect>,
) {
  return prisma.user.findFirst<
    { select: S } & Omit<Prisma.UserFindFirstArgs, 'select' | 'include'>
  >({ select })
}

@millsp do you maybe have an idea how to make that more elegant? You can use https://github.com/timsuchanek/make-type/blob/master/main.ts as a playground

Thanks for the answer @Sytten! Yes I totally think we can add something there. I’ll candidate this for next sprint.