#Routes type any

14 messages · Page 1 of 1 (latest)

novel badgerBOT
#

Hi @cyan parrot. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

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:

  1. No returns validator on the function

    If you don’t specify returns, Convex infers the return type from the handler. When that inference fails (e.g. complex mapping, unions, or use of v.any()), the generated type can degrade to any. Adding an explicit returns validator forces a concrete type instead of any.[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]

  2. Use of v.any() or very generic validators

    If your function’s return type or schema uses v.any() (especially inside v.record), TypeScript can end up with “excessively deep” or fallback any types. There’s a Discord report where v.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.

cyan parrot
#

@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(),
})

novel badgerBOT
#

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:

  1. 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, not boolean. That conflicts with returns: v.boolean() (which is boolean). Convex’s docs show handlers either unannotated or matching the validator’s type, e.g. no explicit return type when using returns.[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,
    
  2. Make sure publicQuery forwards args and returns to query

    In 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 args and returns) is what Convex uses to infer non‑any types 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:

  1. Your custom builders must be used as the function constructors

    The pattern Convex expects (and that convex-helpers is 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 myMutation and userQuery being used.[authorization article; custom functions]

    Your serverMutation/serverQuery/serverAction/adminQuery/adminMutation follow that pattern correctly: they are builders, not actual functions. That part is fine.

  2. customCtx vs inline { args, input }

    The docs show two equivalent styles:

    • Using customCtx when you only modify ctx:

      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 input returns only the additions/overrides, not { ctx, args: {} } with the original ctx object.[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
     }),
   );
  1. For adminQuery/adminMutation, use customCtx exactly like the docs

    Your adminQuery/adminMutation are very close to the userQuery example 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 customCtx merges { user } into the existing ctx; you don’t need to (and shouldn’t) return the whole ctx object. This keeps CustomCtx<typeof adminQuery> and the generated function types precise.[authorization article]

  2. 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, change return { ctx, args: {} } to only return additions/overrides, e.g. return { args: {} } or return { ctx: {}, args: {} }.
  • In adminQuery/adminMutation, with customCtx, 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/returns validators and omit manual handler return type annotations.

If you can paste one of the call sites (e.g. export const getOne = serverQuery({ ... })) plus how it appears in _generated/api.d.ts, I can map exactly which of these is causing the { [x: string]: any } there.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: