Robert
Robert3w ago

Type Issue with gameId in Convex Query

Hi, I have an issue with TypeScript types in Convex. I'm looking at the example code in the Fast5 repo:
import { v } from "convex/values";
import { query } from "../_generated/server";
import { withUser } from "../wrappers/withUser";

export default query({
args: {
gameId: v.id("games"),
},
handler: withUser(async ({ db, user }, { gameId }) => { ... }),
});
import { v } from "convex/values";
import { query } from "../_generated/server";
import { withUser } from "../wrappers/withUser";

export default query({
args: {
gameId: v.id("games"),
},
handler: withUser(async ({ db, user }, { gameId }) => { ... }),
});
In the Fast5 repo, gameId has the proper type:
(parameter) gameId: Id<"games">
(parameter) gameId: Id<"games">
However, when I use the exact same code in my repo, the type of gameId is inferred as:
(parameter) gameId: any
(parameter) gameId: any
I’m wondering why this is happening. Could this be related to TypeScript configuration, Convex version differences, or something else? Any help would be appreciated! Thanks! 😊
44 Replies
Convex Bot
Convex Bot3w 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!
Robert
RobertOP3w ago
withUser wrapper is the same as in Fast5 repo
ballingt
ballingt3w ago
Usually this is because of a typescript error in another file
Robert
RobertOP3w ago
Thanks for the response! I checked the Convex logs, and I'm seeing:
✔ 22:24:30 Convex functions ready! (7s)
✔ 22:24:30 Convex functions ready! (7s)
I don’t see any TypeScript errors in the logs.
Could there be another reason why gameId is inferred as any?
ballingt
ballingt3w ago
One function having circular inference and therefore inferring any can make lots of types be any. When you say exact same code, do you have the same libraries installed, is this just a copy of that repo? Or you have your own code too? Somewhere you have a type that can't be inferred, or some types that are broken (but apparently not in a way that causes a type error)
Robert
RobertOP3w ago
while building i did got one errror can this be the case?
info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules
Linting and checking validity of types ..Failed to compile.

./node_modules/convex-helpers/index.ts:16:22
Type error: Type 'Iterable<FromType>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

14 | let index = 0;
15 | list = await list;
> 16 | for (const item of list) {
| ^
17 | promises.push(asyncTransform(item, index));
18 | index += 1;
19 | }
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules
Linting and checking validity of types ..Failed to compile.

./node_modules/convex-helpers/index.ts:16:22
Type error: Type 'Iterable<FromType>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.

14 | let index = 0;
15 | list = await list;
> 16 | for (const item of list) {
| ^
17 | promises.push(asyncTransform(item, index));
18 | index += 1;
19 | }
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
ballingt
ballingt3w ago
ah it looks like a Next.js typecheck failed? Someone fixed this issue yesterday by adding downlevelIteration to their convex/tsconfig.json, but there's something we'll need to figure out here.
Robert
RobertOP3w ago
Ok, will try and get back to you.
ballingt
ballingt3w ago
Can you share your code for the any issue? Would help to see more examples
Robert
RobertOP3w ago
Example function with wrapper:
const zMutation = zCustomMutation(mutation, NoOp);

export const applyToOffer = zMutation({
args: { data: applicationValidator },
handler: withUser(async (ctx, args) => {
const { projectId } = args.data;

const project = await ctx.db.get(projectId);

if (!project) {
throw new ConvexError({
message: "Project not found",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

await ctx.db.insert("applications", {
...args.data,
status: "PENDING",
});
}),
});
const zMutation = zCustomMutation(mutation, NoOp);

export const applyToOffer = zMutation({
args: { data: applicationValidator },
handler: withUser(async (ctx, args) => {
const { projectId } = args.data;

const project = await ctx.db.get(projectId);

if (!project) {
throw new ConvexError({
message: "Project not found",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

await ctx.db.insert("applications", {
...args.data,
status: "PENDING",
});
}),
});
export const applicationValidator = z.object({
applicantId: zid("users"),
projectId: zid("projects"),
offerId: zid("offers"),
status: applicationStatusValidator,
position: z.string().max(MAX_OFFER_POSITION_INPUT_LENGTH),
message: z.string().max(MAX_APPLICATION_MESSAGE_INPUT_LENGTH),
resume: z.string(),
skills: z.array(z.string()),
experience: z.string(),
updatedAt: z.string(),
});
export const applicationValidator = z.object({
applicantId: zid("users"),
projectId: zid("projects"),
offerId: zid("offers"),
status: applicationStatusValidator,
position: z.string().max(MAX_OFFER_POSITION_INPUT_LENGTH),
message: z.string().max(MAX_APPLICATION_MESSAGE_INPUT_LENGTH),
resume: z.string(),
skills: z.array(z.string()),
experience: z.string(),
updatedAt: z.string(),
});
The withUser is copied from fast5 repo:
import { QueryCtx, MutationCtx, mutation, query } from '../_generated/server';
import { Doc } from '../_generated/dataModel';

/**
* Wrapper for a Convex query or mutation function that provides a user in ctx.
*
* Throws an exception if there isn't a user logged in.
* Pass this to `query`, `mutation`, or another wrapper. E.g.:
* export default mutation({
* handler: withUser(async ({ db, auth, user }, {args}) => {...})
* });
* @param func - Your function that can now take in a `user` in the first param.
* @returns A function to be passed to `query` or `mutation`.
*/
export const withUser = <Ctx extends QueryCtx, Args extends [any] | [], Output>(
func: (ctx: Ctx & { user: Doc<'users'> }, ...args: Args) => Promise<Output>
): ((ctx: Ctx, ...args: Args) => Promise<Output>) => {
return async (ctx: Ctx, ...args: Args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error(
'Unauthenticated call to function requiring authentication'
);
}
// Note: If you don't want to define an index right away, you can use
// db.query("users")
// .filter(q => q.eq(q.field("tokenIdentifier"), identity.tokenIdentifier))
// .unique();
const user = await ctx.db
.query('users')
.withIndex('by_token', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error('User not found');
return func({ ...ctx, user }, ...args);
};
};
import { QueryCtx, MutationCtx, mutation, query } from '../_generated/server';
import { Doc } from '../_generated/dataModel';

/**
* Wrapper for a Convex query or mutation function that provides a user in ctx.
*
* Throws an exception if there isn't a user logged in.
* Pass this to `query`, `mutation`, or another wrapper. E.g.:
* export default mutation({
* handler: withUser(async ({ db, auth, user }, {args}) => {...})
* });
* @param func - Your function that can now take in a `user` in the first param.
* @returns A function to be passed to `query` or `mutation`.
*/
export const withUser = <Ctx extends QueryCtx, Args extends [any] | [], Output>(
func: (ctx: Ctx & { user: Doc<'users'> }, ...args: Args) => Promise<Output>
): ((ctx: Ctx, ...args: Args) => Promise<Output>) => {
return async (ctx: Ctx, ...args: Args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error(
'Unauthenticated call to function requiring authentication'
);
}
// Note: If you don't want to define an index right away, you can use
// db.query("users")
// .filter(q => q.eq(q.field("tokenIdentifier"), identity.tokenIdentifier))
// .unique();
const user = await ctx.db
.query('users')
.withIndex('by_token', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier)
)
.unique();
if (!user) throw new Error('User not found');
return func({ ...ctx, user }, ...args);
};
};
Build with the downlevelIteration as true is still failing: my convex/tsconfig.ts
{
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": false,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,

/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"downlevelIteration": true,
"noEmit": true,

"isolatedModules": true
},
"include": ["./**/*"],
"exclude": ["_generated"]
}
{
"compilerOptions": {
/* These settings are not required by Convex and can be modified. */
"allowJs": true,
"strict": false,
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,

/* These compiler options are required by Convex */
"target": "ESNext",
"lib": ["ES2021", "dom"],
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"downlevelIteration": true,
"noEmit": true,

"isolatedModules": true
},
"include": ["./**/*"],
"exclude": ["_generated"]
}
My next.js version is 14.2.7 and node 22.13.1 So I updated dependencies and cleared cache and the problem is no longer appearing but the args still have type any
export const applyToOffer = zMutation({
args: { data: applicationValidator },
handler: withUser(async (ctx, args) => {
const userId = await getAuthUserId(ctx);

const { projectId } = args.data;

const project = await ctx.db.get(projectId);

if (!project) {
throw new ConvexError({
message: "Project not found",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

await ctx.db.insert("applications", {
...args.data,
status: "PENDING",
applicantId: userId,
});
}),
});
export const applyToOffer = zMutation({
args: { data: applicationValidator },
handler: withUser(async (ctx, args) => {
const userId = await getAuthUserId(ctx);

const { projectId } = args.data;

const project = await ctx.db.get(projectId);

if (!project) {
throw new ConvexError({
message: "Project not found",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

await ctx.db.insert("applications", {
...args.data,
status: "PENDING",
applicantId: userId,
});
}),
});
ballingt
ballingt3w ago
@Robert sorry, I should have been more specific. Could you share all the code, as a GitHub repo? Often the problem is not just with one file, it's an any in another file that causes a propagation of any types via the schema or api object types. We'll see an any coming from somewhere if we look through all the files
Robert
RobertOP3w ago
Sure, I can share the code, but not publicly. Let me know how you'd like to proceed—DM with link or adding you to a private repo?
ballingt
ballingt3w ago
Sure, you can add me, GitHub handle is thomasballinger. I'l write up my method to add it to the docs feel free to email tom@convex.dev or support@convex.dev
Robert
RobertOP3w ago
ok i will add you in a sec I've sent an invite
ballingt
ballingt3w ago
@Robert what file do you see this issue in? I don't see a gameId
Robert
RobertOP3w ago
convex/PROJECT_FUNCTIONS/applications.ts the gameId was an example that this happends even if i do it on the 1:1 example from fast5 repo
ballingt
ballingt3w ago
I can't find one tha'ts a problem, what's a line where you see an issue?
ballingt
ballingt3w ago
No description
ballingt
ballingt3w ago
what package manager do you use, I see yarn.lock and a package-lock.json
Robert
RobertOP3w ago
line 16 I use yarn
Robert
RobertOP3w ago
No description
ballingt
ballingt3w ago
No description
ballingt
ballingt3w ago
tried with yarn too, same thing hm do you have missing dependnecies, what version of TypeScript are you using? you want to track down where an any is coming from, perhaps from an import you might want to use strict mode in TypeScript strict mode highlights anys much more
Robert
RobertOP3w ago
I see that you are showing the userId type which is fine with displaying but i am talking about
any
any
in
handler: withUser(async (ctx, args) =>
handler: withUser(async (ctx, args) =>
- args are type
any
any
ballingt
ballingt3w ago
what line number?
Robert
RobertOP3w ago
based on this screen the line 14
ballingt
ballingt3w ago
You might not have pushed recently, I don't see any withUser
Robert
RobertOP3w ago
I forgot to say that this is on dev branch, sorry
ballingt
ballingt3w ago
ah ok I see it now
ballingt
ballingt3w ago
your zod validator seems convertible
No description
ballingt
ballingt3w ago
so nothing's sticking out yet I'll get back to think in a few min
ballingt
ballingt3w ago
so the zod stuff is fine without withUser
No description
ballingt
ballingt3w ago
@Robert you said you copied this fancy withUser thing from Fast5? That's pretty old code
ballingt
ballingt3w ago
last updated 2 years ago
No description
ballingt
ballingt3w ago
GitHub
convex-helpers/packages/convex-helpers/README.md at main · get-conv...
A collection of useful code to complement the official packages. - get-convex/convex-helpers
Robert
RobertOP3w ago
Ok i will recreate the withUser using custom functions and see if this will work
ballingt
ballingt3w ago
all of this got rewritten when we added argument validators I think that's what you're running into
ballingt
ballingt3w ago
Customizing serverless functions without middleware
Re-use code and centralize request handler definitions with discoverability and type safety and without the indirection of middleware or nesting of wr...
ballingt
ballingt3w ago
another option is not doing this, and writing a helper function that you use like const currentUser = await getCurrentUser(ctx) but since it's just code, you can get fancy with it if you want, that's what custom functions are but the TypeScript is pretty fancy, personally I prefer calling helper functions
Robert
RobertOP3w ago
ok, i see. I will try with custom functions, i think this will work perfectly. Thanks for help and getting through my messy code 😄
ballingt
ballingt3w ago
I think there might be another version of custom functions specifically for the zod thingyou're doing ah it's called zCustomFunction https://stack.convex.dev/typescript-zod-function-validation
Robert
RobertOP3w ago
That's good to know, will try
ballingt
ballingt3w ago
something like this
No description
Robert
RobertOP3w ago
Now it is working! Sharing a zMutationWithUser example in case someone encounters a similar issue in the future
import { getAuthUserId } from "@convex-dev/auth/server";
import { ConvexError } from "convex/values";
import { zCustomMutation } from "convex-helpers/server/zod";

import { mutation } from "../_generated/server";

export const zMutationWithUser = zCustomMutation(mutation, {
args: {},
input: async (ctx, args) => {
const userId = await getAuthUserId(ctx);

if (!userId) {
throw new ConvexError({
message: "Unauthorized",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

const user = await ctx.db.get(userId);

if (!user) {
throw new ConvexError({
message: "User not found",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

return { ctx: { user }, args };
},
});
import { getAuthUserId } from "@convex-dev/auth/server";
import { ConvexError } from "convex/values";
import { zCustomMutation } from "convex-helpers/server/zod";

import { mutation } from "../_generated/server";

export const zMutationWithUser = zCustomMutation(mutation, {
args: {},
input: async (ctx, args) => {
const userId = await getAuthUserId(ctx);

if (!userId) {
throw new ConvexError({
message: "Unauthorized",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

const user = await ctx.db.get(userId);

if (!user) {
throw new ConvexError({
message: "User not found",
code: "INTERNAL_SERVER_ERROR",
statusCode: 500,
severity: "error",
});
}

return { ctx: { user }, args };
},
});
Thanks for help!

Did you find this page helpful?