Dan6erbond
Dan6erbond2w ago

TypeScript not Inferring Query `handler` Type Correctly

Hey everyone, I'm trying to "JOIN" some data in my query in order to include the creator directly in my response. However, it seems like TypeScript isn't able to properly infer the handler's return type even with excessive type hints:
export const find = query({
args: {
paginationOpts: paginationOptsValidator,
filter: v.optional(
v.object({
createdById: v.optional(v.id("users")),
search: v.optional(v.string()),
})
),
},
handler: async (ctx, args) => {
let query = ctx.db.query("protests");

if (args.filter?.search) {
return await query
.withSearchIndex("fts", (q) => q.search("fts", args.filter!.search!))
.paginate(args.paginationOpts);
}

if (args.filter?.createdById) {
query = query.filter((q) =>
q.eq(q.field("createdById"), args.filter?.createdById)
);
}

const { page, ...res } = await query.paginate(args.paginationOpts);

const users: (Doc<"users"> & {
profileImageUrl?: string;
})[] = await Promise.all(
page
.reduce(
(ids, protest) =>
ids.indexOf(protest.createdById) !== -1
? ids
: [...ids, protest.createdById],
[] as Id<"users">[]
)
.map(
(id) =>
ctx.runQuery(api.users.findById, { id }) as Promise<
Doc<"users"> & {
profileImageUrl?: string;
}
>
)
);

return {
...res,
page: page.map(
(protest) =>
({
...protest,
createdBy: users.find((u) => u?._id === protest.createdById),
}) as Doc<"protests"> & {
createdBy: Doc<"users"> & {
profileImageUrl?: string;
};
}
),
};
},
returns: v.object({
isDone: v.boolean(),
continueCursor: v.string(),
splitCursor: v.optional(v.union(v.string(), v.null())),
pageStatus: v.optional(
v.union(
v.literal("SplitRecommended"),
v.literal("SplitRequired"),
v.null()
)
),
page: v.array(
v.object({
_id: v.id("protests"),
_creationTime: v.number(),
name: v.string(),
startDate: v.number(),
endDate: v.optional(v.number()),
createdById: v.id("users"),
mainLocationId: v.optional(v.id("locations")),
missionStatement: v.string(),
fts: v.string(),
createdBy: v.object({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
profileImageId: v.optional(v.id("_storage")),
profileImageUrl: v.optional(v.string()),
}),
})
),
}),
});
export const find = query({
args: {
paginationOpts: paginationOptsValidator,
filter: v.optional(
v.object({
createdById: v.optional(v.id("users")),
search: v.optional(v.string()),
})
),
},
handler: async (ctx, args) => {
let query = ctx.db.query("protests");

if (args.filter?.search) {
return await query
.withSearchIndex("fts", (q) => q.search("fts", args.filter!.search!))
.paginate(args.paginationOpts);
}

if (args.filter?.createdById) {
query = query.filter((q) =>
q.eq(q.field("createdById"), args.filter?.createdById)
);
}

const { page, ...res } = await query.paginate(args.paginationOpts);

const users: (Doc<"users"> & {
profileImageUrl?: string;
})[] = await Promise.all(
page
.reduce(
(ids, protest) =>
ids.indexOf(protest.createdById) !== -1
? ids
: [...ids, protest.createdById],
[] as Id<"users">[]
)
.map(
(id) =>
ctx.runQuery(api.users.findById, { id }) as Promise<
Doc<"users"> & {
profileImageUrl?: string;
}
>
)
);

return {
...res,
page: page.map(
(protest) =>
({
...protest,
createdBy: users.find((u) => u?._id === protest.createdById),
}) as Doc<"protests"> & {
createdBy: Doc<"users"> & {
profileImageUrl?: string;
};
}
),
};
},
returns: v.object({
isDone: v.boolean(),
continueCursor: v.string(),
splitCursor: v.optional(v.union(v.string(), v.null())),
pageStatus: v.optional(
v.union(
v.literal("SplitRecommended"),
v.literal("SplitRequired"),
v.null()
)
),
page: v.array(
v.object({
_id: v.id("protests"),
_creationTime: v.number(),
name: v.string(),
startDate: v.number(),
endDate: v.optional(v.number()),
createdById: v.id("users"),
mainLocationId: v.optional(v.id("locations")),
missionStatement: v.string(),
fts: v.string(),
createdBy: v.object({
name: v.optional(v.string()),
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerificationTime: v.optional(v.number()),
phone: v.optional(v.string()),
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
profileImageId: v.optional(v.id("_storage")),
profileImageUrl: v.optional(v.string()),
}),
})
),
}),
});
The error I get from TypeScript is:
Type 'Promise<PaginationResult<...>>' is not assignable to type 'ValidatorTypeToReturnType<{ splitCursor?: string | null | undefined; pageStatus?: "SplitRecommended" | "SplitRequired" | null | undefined; isDone: boolean; continueCursor: string; page: { endDate?: number | undefined; ... 8 more ...; createdBy: { ...; }; }[]; }>'.
...
Property 'createdBy' is missing in type '{ _id: Id<"protests">; _creationTime: number; endDate?: number | undefined; mainLocationId?: Id<"locations"> | undefined; name: string; startDate: number; createdById: Id<"users">; missionStatement: string; fts: string; }' but required in type '{ endDate?: number | undefined; mainLocationId?: Id<"locations"> | undefined; name: string; _creationTime: number; startDate: number; createdById: Id<"users">; missionStatement: string; fts: string; _id: Id<...>; createdBy: { ...; }; }'.ts(2719)
Type 'Promise<PaginationResult<...>>' is not assignable to type 'ValidatorTypeToReturnType<{ splitCursor?: string | null | undefined; pageStatus?: "SplitRecommended" | "SplitRequired" | null | undefined; isDone: boolean; continueCursor: string; page: { endDate?: number | undefined; ... 8 more ...; createdBy: { ...; }; }[]; }>'.
...
Property 'createdBy' is missing in type '{ _id: Id<"protests">; _creationTime: number; endDate?: number | undefined; mainLocationId?: Id<"locations"> | undefined; name: string; startDate: number; createdById: Id<"users">; missionStatement: string; fts: string; }' but required in type '{ endDate?: number | undefined; mainLocationId?: Id<"locations"> | undefined; name: string; _creationTime: number; startDate: number; createdById: Id<"users">; missionStatement: string; fts: string; _id: Id<...>; createdBy: { ...; }; }'.ts(2719)
Can someone assist in this on how I can best JOIN the creator?
2 Replies
Convex Bot
Convex Bot2w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
ballingt
ballingt2w ago
At what stage does the inference fail, can you find the first place where the type is too general?

Did you find this page helpful?