#TS2589
10 messages · Page 1 of 1 (latest)
Hi @viscid goblet. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
TS2589 (“Type instantiation is excessively deep and possibly infinite”) with Convex almost always comes from circular type inference around ctx.runQuery / ctx.runMutation / ctx.runAction and Convex’s generated types.
From the sources, here are the concrete best practices:
1. Don’t rely on inferred return types through ctx.run*
If an action’s return type depends (even indirectly) on ctx.runQuery / ctx.runMutation, TypeScript can’t resolve the types and you get TS2589. [Circular inference]
Instead, break the inference loop by adding an explicit type annotation:
// Option 1: type the handler return
export const myAction = action({
args: {},
handler: async (ctx): Promise<null> => {
const result = await ctx.runQuery(api.myFunctions.getSomething);
return result;
},
});
or
// Option 2: type the result of ctx.runQuery
export const myAction = action({
args: {},
handler: async (ctx) => {
const result: null = await ctx.runQuery(api.myFunctions.getSomething);
return result;
},
});
This keeps TypeScript from trying to infer the action’s return type through the Convex-generated types of the called function. [Circular inference]
2. Don’t use Convex-generated types in those annotations
From the Discord threads: if your explicit types use Doc, Id, or anything from convex/_generated, you’re still in the same circular graph. [TS2589 thread; TS2589 thread 2]
So for handlers that call ctx.run*:
- Avoid:
// This can still cause TS2589
const requisitionInDb: Doc<"requisitions"> =
await ctx.runQuery(internal.some.query, { ...args });
return requisitionInDb;
-
Prefer:
-
Return a manually-typed subset of fields:
const requisitionInDb: { field: string; otherField: number; } = (await ctx.runQuery(internal.some.query, { ...args })) || {}; return requisitionInDb; -
Or if you only need a couple of props, just type those:
const ongoingBookings: { usedProp: string; otherUsedProp: number }[] = await ctx.runQuery(internal.services.booking.getOngoing, { userId: ctx.user._id, });
-
This “minimal typing” approach is explicitly recommended in the Discord guidance. [TS2589 thread; TS2589 thread 2]
3. returns validators don’t fix TS2589
Adding a returns: v.* validator on the Convex function does not break the circular inference; the problem is at the TypeScript level in the handler’s return type / ctx.run* call. [TS2589 thread 1; TS2589 thread 2]
You must:
- Type the handler return or the
ctx.run*result explicitly, and - Ensure that explicit type does not depend on Convex-generated types.
4. How to track down the offending code
From support’s advice:
- Search for all usages of
ctx.runQuery,ctx.runMutation,ctx.runAction. - Focus on recently changed ones.
- For each, ensure:
- The handler’s return type is explicitly annotated, or
- The
ctx.run*result is explicitly typed, - And those annotations don’t use
Doc,Id, or other generated types. [TS2589 thread]
If you paste a specific function that’s triggering TS2589, I can help rewrite it using these patterns.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@viscid goblet Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Convex 1.33.0 has a fix for one cause of this
@somber plover only the same folder & file combo issue right?
Our issue could possibly be extensive use of ctx.run* and deep types on our functions
Atm our convex experience is getting fairly bad as we’re losing the end to end types via these workarounds..
Are you adding return type annotations to your handler functions? That almost always fixes it for me
@somber plover so in addition to the returns validator we also do something like this?
handler: async (ctx): Promise<ReturnType> => …
Is this also relevant to the problem?