#Routes type any
14 messages · Page 1 of 1 (latest)
This usually happens when Convex can’t infer a specific return type for your function, so it falls back to any in the generated API.
From the docs and examples, there are two common causes and fixes:
-
No
returnsvalidator on the functionIf you don’t specify
returns, Convex infers the return type from the handler. When that inference fails (e.g. complex mapping, unions, or use ofv.any()), the generated type can degrade toany. Adding an explicitreturnsvalidator forces a concrete type instead ofany.[values v; validation any]import { query } from "./_generated/server"; import { v } from "convex/values"; export const getOne = query({ args: { id: v.id("chats") }, returns: v.nullable( v.object({ _id: v.id("chats"), _creationTime: v.number(), // ...other fields }) ), handler: async (ctx, args) => { return await ctx.db.get(args.id); }, });For components, the docs explicitly recommend adding both argument and return validators to avoid
any.[components tips] -
Use of
v.any()or very generic validatorsIf your function’s return type or schema uses
v.any()(especially insidev.record), TypeScript can end up with “excessively deep” or fallbackanytypes. There’s a Discord report wherev.record(v.string(), v.any())was the culprit; changing that validator removed the type issues.[discord deep]If you have something like:
args: { metadata: v.optional(v.record(v.string(), v.any())), },
try replacing v.any() with a more specific validator (e.g. v.string(), v.number(), v.union(...), or a structured v.object(...)).[schemas validators]
If you share one of the new queries that shows up as { [x: string]: any } in your generated api, I can point to the exact validator / return type that needs tightening.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@cyan parrot Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@novel badger
import { v } from "convex/values";
import { publicQuery } from "@/procedures";
const args = {
id: v.id('images'),
}
export const unauth = publicQuery({
args, handler: (ctx, args): true => true,
returns: v.boolean(),
})
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don’t describe your custom publicQuery helper, so I can’t see exactly how it’s typed, but there are two clear issues in this snippet that can cause any-ish behavior:
-
Handler return type annotation is wrong / unnecessary
handler: (ctx, args): true => true,Here you’re annotating the handler’s return type as the literal type
true, notboolean. That conflicts withreturns: v.boolean()(which isboolean). Convex’s docs show handlers either unannotated or matching the validator’s type, e.g. no explicit return type when usingreturns.[validation]Fix:
export const unauth = publicQuery({ args, returns: v.boolean(), handler: (ctx, args) => true, });or, if you really want an annotation:
handler: (ctx, args): boolean => true, -
Make sure
publicQueryforwardsargsandreturnstoqueryIn the official API, you’d write:
import { query } from "./_generated/server"; import { v } from "convex/values"; export const unauth = query({ args: { id: v.id("images") }, returns: v.boolean(), handler: (ctx, args) => true, });This pattern (constructor with
argsandreturns) is what Convex uses to infer non‑anytypes for arguments and return values.[query constructor; validation]
If your publicQuery wrapper doesn’t preserve those generics/validators when it calls query/queryGeneric, TypeScript will fall back to any for the return type.[queryGeneric; ReturnValueForOptionalValidator]
So:
import { v } from "convex/values";
import { publicQuery } from "@/procedures";
const args = {
id: v.id("images"),
};
export const unauth = publicQuery({
args,
returns: v.boolean(),
handler: (ctx, args) => true,
});
If unauth still shows up as { [x: string]: any }, the missing piece is how publicQuery is typed; the docs don’t include that, so I can’t say more confidently than that it must be losing the returns validator’s type information when wrapping query.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@cyan parrot Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Let me check the knowledge base for you again. One moment! :books:
The knowledge sources don’t show your exact serverMutation/serverQuery/adminQuery code, but they do cover customQuery/customMutation/customAction and customCtx, and how to keep types from degrading to any.[custom functions; authorization article]
Given your earlier symptom (“routes type any” / { [x: string]: any }), the main things to check in this Part 2 snippet are:
-
Your custom builders must be used as the function constructors
The pattern Convex expects (and that
convex-helpersis designed for) is:// define a custom builder export const myMutation = customMutation(mutation, { ... }); // later, use it like `mutation` export const doThing = myMutation({ args: { /* real args */ }, returns: v.boolean(), handler: async (ctx, args) => { ... }, });This is exactly how the docs show
myMutationanduserQuerybeing used.[authorization article; custom functions]Your
serverMutation/serverQuery/serverAction/adminQuery/adminMutationfollow that pattern correctly: they are builders, not actual functions. That part is fine. -
customCtxvs inline{ args, input }The docs show two equivalent styles:
-
Using
customCtxwhen you only modifyctx:const userQuery = customQuery( query, customCtx(async (ctx) => { const user = await getUser(ctx); if (!user) throw new Error("Authentication required"); return { user, db: wrappedDb }; }), );
-
Here the customCtx callback returns only the extra ctx fields; the helper takes care of merging them into ctx and preserving types.[custom functions]
-
Using
{ args, input }when you want to intercept args:export const myMutation = customMutation(mutation, { args: { alwaysRequiredArg: v.string(), alwaysOptionalArg: v.optional(v.number()), }, input: async (ctx, args) => { const user = await commonAuthorizationLogic(ctx, args); const wrappedDB = wrapDatabaseAccess(ctx, args); return { ctx: { db: wrappedDB, user }, args: { ...args }, }; }, });Note that
inputreturns only the additions/overrides, not{ ctx, args: {} }with the originalctxobject.[authorization article]
In your server* builders you’re doing:
return { ctx, args: {} };
That’s allowed by the helper’s runtime contract, but it’s not how the examples are written, and the type helpers (CustomCtx<typeof serverMutation>, etc.) are designed around “return only what you’re adding/overriding”. Returning the whole ctx object can confuse the generic inference and is a likely source of “everything is any”.
Prefer:
export const serverMutation = customMutation(mutation, {
args: { _serverKey: v.string() },
input: async (ctx, { _serverKey }) => {
if (_serverKey !== env.SERVER_PRIVATE_KEY) throw new Error("Invalid API key");
return { ctx: {}, args: {} }; // or just `return { args: {} }`
},
});
or, if you don’t need to touch args at all, use customCtx instead:
export const serverMutation = customMutation(
mutation,
customCtx(async (ctx) => {
// you can’t see args here; this is ctx-only
return {}; // no ctx changes
}),
);
-
For
adminQuery/adminMutation, usecustomCtxexactly like the docsYour
adminQuery/adminMutationare very close to theuserQueryexample in the article; the only difference is that you’re returning{ ctx, args: {} }instead of just the extra ctx fields.[custom functions]The recommended pattern is:
export const adminQuery = customQuery( query, customCtx(async (ctx) => { const data = await userCheckOrThrow(ctx); if (data.user.role !== "ADMIN") throw new Error("Unauthorized"); // if you want to expose `user` on ctx: return { user: data.user }; }), ); export const adminMutation = customMutation( mutation, customCtx(async (ctx) => { const data = await userCheckOrThrow(ctx); if (data.user.role !== "ADMIN") throw new Error("Unauthorized"); return { user: data.user }; }), );Here
customCtxmerges{ user }into the existing ctx; you don’t need to (and shouldn’t) return the wholectxobject. This keepsCustomCtx<typeof adminQuery>and the generated function types precise.[authorization article] -
Downstream functions should rely on validators, not manual annotations
When you later define:
export const getOne = serverQuery({ args: { id: v.id("images") }, returns: v.boolean(), handler: async (ctx, args) => true, });
you should not add a manual return type annotation on handler; Convex infers it from returns.[validation] Manual annotations that don’t match (e.g. : true) can break inference and contribute to any.
Summary of concrete changes to reduce any types:
- In
serverMutation/serverQuery/serverAction, changereturn { ctx, args: {} }to only return additions/overrides, e.g.return { args: {} }orreturn { ctx: {}, args: {} }. - In
adminQuery/adminMutation, withcustomCtx, return only the extra ctx fields (e.g.{ user: data.user }), not{ ctx, args: {} }. - In the actual queries/mutations you build with these helpers, rely on
args/returnsvalidators and omit manual handler return type annotations.