[SOLVED] Object relation works when running app but doesn't compile to TypeScript?

My database schema looks as follows:
entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User? @relation(fields: [userId], references: [id])
userId Int?
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
psl=}

entity Category {=psl
id Int @id @default(autoincrement())
name String
tasks Task[]
psl=}
entity Task {=psl
id Int @id @default(autoincrement())
description String
isDone Boolean @default(false)
user User? @relation(fields: [userId], references: [id])
userId Int?
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
psl=}

entity Category {=psl
id Int @id @default(autoincrement())
name String
tasks Task[]
psl=}
Meanwhile on my client when I do:
<span className={task.isDone ? "line-through text-black" : "text-black"}>
{task.description} ({task.category.name})
</span>
<span className={task.isDone ? "line-through text-black" : "text-black"}>
{task.description} ({task.category.name})
</span>
I get the following TypeScript error under task.category.name: Property 'category' does not exist on type 'GetResult<{ id: number; description: string; isDone: boolean; userId: number | null; categoryId: number; }, unknown> & {}'. Did you mean 'categoryId'?ts(2551) I have seen examples within the Wasp repo that allow this to work. This is done by using the includes part of the ORM like so:
import { Task } from "@wasp/entities";
import { GetAllUserTasksQuery } from "@wasp/queries/types";
import HttpError from "@wasp/core/HttpError.js";

export const getAllUserTasksQuery: GetAllUserTasksQuery<void, Task[]> = async (
args,
context
) => {
if (!context.user) {
throw new HttpError(401);
}

return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
include: { category: true },
orderBy: { id: "asc" },
});
};
import { Task } from "@wasp/entities";
import { GetAllUserTasksQuery } from "@wasp/queries/types";
import HttpError from "@wasp/core/HttpError.js";

export const getAllUserTasksQuery: GetAllUserTasksQuery<void, Task[]> = async (
args,
context
) => {
if (!context.user) {
throw new HttpError(401);
}

return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
include: { category: true },
orderBy: { id: "asc" },
});
};
However despite this, it still seems like I am missing a trick to get the type safety to understand that a Task is expected to reference Category (object) as well as the CategoryId? This is also blocking me from successfully deploying to Fly.io Thanks
11 Replies
martinsos
martinsos12mo ago
Thanks for reporting this @KYAN1TE , we will take a look into it! @miho @Filip do you have any ideas what might be causing this issue?
KYAN1TE
KYAN1TEOP12mo ago
No worries, here is the repo btw: https://github.com/karam94/WaspTodoApp
Filip
Filip12mo ago
Hi @KYAN1TE, I can try to run your repro repo and test a solution later, but I think I know the fix right away. Let me know if it works. I'll dig deeper if not 🙂 TL;DR: Use the satisfies keyword. The problem You annotated the Query with GetAllUserTasksQuery<void, Task[]> which tells Wasp "This query doesn't receieve any arguments and returns an array of Task objects." An object of type Task truly doesn't contain the Category. So, strictly speaking, the error is correct. The solution I'm guessing that you want TypeScript to infer the correct return type from the findMany call. If so, instead of annotating the query with GetAllUserTasksQuery<void, Task[]>, try using the satisfies keyword. Like this:
export const getAllUserTasksQuery = (async (
args,
context
) => {
if (!context.user) {
throw new HttpError(401);
}

return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
include: { category: true },
orderBy: { id: "asc" },
});
}) satisfies GetAllUserTasksQuery<void>;
export const getAllUserTasksQuery = (async (
args,
context
) => {
if (!context.user) {
throw new HttpError(401);
}

return context.entities.Task.findMany({
where: { user: { id: context.user.id } },
include: { category: true },
orderBy: { id: "asc" },
});
}) satisfies GetAllUserTasksQuery<void>;
This tells TypeScript: "Make sure this function doesn't take any arguments and works with the proper context object, but infer the correct return type on your own."
Filip
Filip12mo ago
Our docs used to mention this, but we removed it thinking it was too much of an edge case. Here's the relevant section from the old docs:
No description
KYAN1TE
KYAN1TEOP12mo ago
Thanks for the response @Filip Unfortunately, I still getting an error on the client/JSX code for task.category.name despite applying your recommended changes to the getAllUserTasksQuery. I believe this is simply because despite the change on the server, my React component's prop expects a type Task (which by default doesn't contain the category object, only the id) & so my next question would be, in this case, how are we expected to match types between server & client in this case? Kind of feel like unless there is an obvious solution for this that I'm missing, that it makes it a bit difficult to pass queried data down as typed props & that there is an expectation to always query data in the component where it is required, which makes it difficult to build front-ends up as different reusable components 🤔 Thanks
MEE6
MEE612mo ago
Wohooo @KYAN1TE, you just became a Waspeteer level 3!
KYAN1TE
KYAN1TEOP12mo ago
So I've fixed it by having a play. Created a types.ts in the server with:
export type TaskWithCategory = Task & {
category: Category;
};
export type TaskWithCategory = Task & {
category: Category;
};
& then I just use that within my getAllUserTasksQuery without satisfies like so:
export const getAllUserTasksQuery: GetAllUserTasksQuery<
void,
TaskWithCategory[]
>
export const getAllUserTasksQuery: GetAllUserTasksQuery<
void,
TaskWithCategory[]
>
& then that lets me define my client props like so to avoid the error client side:
interface TaskComponentProps {
task: TaskWithCategory;
}
interface TaskComponentProps {
task: TaskWithCategory;
}
Note that I'm a C# guy, not a TypeScript guy & I don't know much about wasp (yet) so I am all ears if there is a better/more maintainable way to do this. Thanks Another update, so the Fly deployment with the above supposed solution isn't functional:
"src/ext-src/components/Notepad/TaskComponent.tsx(7,34): error TS2307: Cannot find module '../../../server/types' or its corresponding type declarations.\n"
"src/ext-src/components/Notepad/TaskComponent.tsx(7,34): error TS2307: Cannot find module '../../../server/types' or its corresponding type declarations.\n"
It's pretty self explanatory & understandable if it's trying to deploy client/server separately (unsure exactly how wasp is compiling then deploying under the hood) but as it's late, I'll not bother trying to solve (except the obvious of just having two separate types on server & client which has dedployed successfully) & just see what you suggest tomorrow morning 🙂 Thanks again
Filip
Filip12mo ago
Hey @KYAN1TE, First of all, massive props for documenting and explaining the problem perfectly! It's always a joy helping users who put an effort into communication (and they're not exactly a common occurance 😅). I'll make sure to take a deep dive into this first thing tomorrow!
I believe this is simply because despite the change on the server, my React component's prop expects a type Task
This is correct. You'd have to tell the component to except something else (i.e., a Task object that expects a category), which is what you did.
how are we expected to match types between server & client in this case?
Ideally, Wasp would expose types for all possible objects different Prisma queries can return. Unfortunately, this would amount to an almost infinite amount of types, so we're still figuring out how to support something like that 😅 With that in mind, our current options are: - Option 1: Use the Query's return type as a source of truth and rely on type inference for typing the client stuff. - Option 2: Explicitly define the payload type in a single place and import/reuse it on both the client and the server. - Option 3: Define different types on the server and on the client. You attempted options 1 and 2, but the code wouldn't compile. You then successfully implemented option 3. Let's try to figure out what went wrong with the first two. Option 1 Gist link: https://gist.github.com/sodic/211a5e719163505e4d0c5dbb8d50d693. The gist contains all the changed files. You can delete types.ts on the server. Option 2 Coming in a bit 🙂
KYAN1TE
KYAN1TEOP12mo ago
Amazing, thank you so much @Filip! I look forward to option 2 but can live with having to use satisfies for now 👍
Filip
Filip12mo ago
Option 2 Hey, finally got around to fixing up option 2: https://gist.github.com/sodic/79baf87800ab1a485ab6b6551fb40956 In short, for sharing files between the server and the client, you can use the shared folder (we mention it here: https://wasp-lang.dev/docs/tutorial/project-structure). Your IDE will report an import error in src/shared/types.ts, but you can ignore it. It won't come up during the build. This is a mistake on our side, thanks for sticking with us regardless of these rough edges 🙃). Extra stuff If you're annoyed by the red squiggly line, you can update the paths field in src/shared/tsconfig.json to fix it:
"paths": {
"@wasp/*": [
"src/*"
],
"*": [
"node_modules/*",
"node_modules/@types/*"
]
},
// ... keep the rest of the file unchanged
"paths": {
"@wasp/*": [
"src/*"
],
"*": [
"node_modules/*",
"node_modules/@types/*"
]
},
// ... keep the rest of the file unchanged
I won't go into detail about what's going on here, but the short version is: This false positive import error is a bug on our side. We had to choose whether we want false positives or false negatives in the IDE. We choose to go with false positives. You can instead opt for false negatives by implementing the tsconfig outlined above. Just be careful: by doing so you might lose compile-time reports for some real type errors.
KYAN1TE
KYAN1TEOP12mo ago
Thank you 🙂
Want results from more Discord servers?
Add your server