typescriptprismatrpc

Why is TypeScript not recognizing included fields with Prisma and TRPC


I'm using Prisma with TRPC and TypeScript, and I'm encountering a TypeScript warning when trying to include related models in my query. Even though the query runs correctly and returns the expected data, VSCode gives me the following error:

Property 'posts' does not exist on type '{ id: string; id: string; }'.

Here is a simplified version of my code:

const includeFields = {
  posts: true,
};

type UserWithPosts = Prisma.UserGetPayload<{ include: typeof includeFields }>;

const { data, isLoading } = trpc.user.findMany.useQuery<UserWithPosts[]>(
  { include: includeFields },
  {}
);

if (!isLoading && data) {
  const firstUser = data[0];
  if (firstUser) {
    console.log(firstUser.posts); // <-- I'm getting a warning here
  }
}

Here is the relevant part of my Prisma schema defining the relationship between User and Post:

model User {
  id    String  @id @default(cuid())
  name  String
  posts Post[]  // Relation to the Post model
}

model Post {
  id     String @id @default(cuid())
  title  String
  userId String
  user   User   @relation(fields: [userId], references: [id])
}

What I Expected:

I was expecting TypeScript to infer that posts exists on the UserWithPosts type based on the include object passed to Prisma. Since I'm including the posts relation in the query, I thought TypeScript would recognize that posts is available in the resulting data.

What I've Tried:

Additional Observation:

This works as expected on the server side when using prisma.user.findMany:

const users = await prisma.user.findMany({ include: { posts: true } });
console.log(users?.[0]?.posts); // <-- The type is correctly inferred here.

Update:

I've realized that the issue lies in my router definition:

export const usersRouter = t.router({
  findMany: t.procedure.input(UserFindManySchema).query(async ({ ctx, input }) => {
    return await ctx.prisma.user.findMany(input);
  })
});

The findMany function is not defined to be generic, which causes the type information to get lost. I don't know how to fix that, though.

EDIT: Updated the error message, added information about prisma queries.

EDIT 2: I added information about the router.


Solution

  • Let's look at that router.

    export const usersRouter = t.router({
      findMany: t.procedure.input(UserFindManySchema).query(async ({ ctx, input }) => {
        return await ctx.prisma.user.findMany(input);
      })
    });
    

    Here UserFindManySchema is all possible options. It's include options could be anything. And since this query is not a generic function, the input types can't affect the output types.

    Also, tRPC can't have generic queries as it would require a feature typescript doesn't have.


    Additionally, and this is important, exposing your entire database query parameters to the client is very bad idea. You are basically exposing your entire database to the client since they could include relations that span every table. They could run huge queries that crash your database, or worse, gain access to secrets like user password hashes or api keys. So what your trying to do is a very bad idea.


    So you have two choices:

    1. Be ok with the field optionally existing.

    This means that your tRPC returns a type where the relation is optional, and then you have to check for the property before you use it:

    // server
    export const usersRouter = t.router({
      findMany: t.procedure.input({ includePosts: z.boolean() }).query(async ({ ctx, input }) => {
        return await ctx.prisma.user.findMany({ include: { posts: includePosts } });
      })
    });
    
    // client
    const { data, isLoading } = trpc.user.findMany.useQuery(
      { includePosts: true },
    );
    
    if (!isLoading && data) {
      const firstUser = data[0];
      if (firstUser && 'posts' in firstUser) {
        console.log(firstUser.posts);
      }
    }
    

    You may be able to wrap that useQuery call in a custom hook with a type guard to do this check and return the right type. But that would need to happen outside of tRPC and react query entirely.

    1. Or you break it up into two endpoints:
    export const usersRouter = t.router({
      findMany: t.procedure.query(async ({ ctx, input }) => {
        return await ctx.prisma.user.findMany();
      })
    
      findManyWithPosts: t.procedure.query(async ({ ctx, input }) => {
        return await ctx.prisma.user.findMany({
          includes: { posts: true }
        });
      });
    });
    

    And now each endpoint has a different return type.