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])
}
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.
as const
to the includeFields
object based on a suggestion.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.
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.
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:
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.
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.