get-convex / convex-js

TypeScript/JavaScript client library for Convex
https://docs.convex.dev
Apache License 2.0
94 stars 12 forks source link

Convex Actions - returning anything from "runQuery" or "runMutations" ends with circular reference error #3

Closed Willaiem closed 1 year ago

Willaiem commented 1 year ago

When creating the app for the Web Dev Cody's hackathon, I found that returning anything from the ctx.runQuery or ctx.runMutation when using the Convex Actions ends with "circularly references" error.

reproduction repo: https://github.com/Willaiem/convex-ts-bug

Reproduction steps:

  1. Create the Vite app with React with npm init vite@latest and choose React + TS.
  2. Install the dependencies.
  3. Run npx convex dev
  4. Create actions.ts and paste the code below.

actions.ts

import { action, internalQuery } from "./_generated/server";
import { internal } from "./_generated/api";

export const getTasks = internalQuery({
    args: {},
    handler: async (ctx) => {
        const tasks = await ctx.db.query('tasks').collect()

        return tasks
    }
})

export const invokeAction = action({
    args: {},
    handler: async (ctx) => {
        const tasks = await ctx.runQuery(internal.actions.getTasks)

        const texts = tasks.map(task => task.text)

        // do something else...

        return texts 
    }
})

The error from the Convex CLI:

PS E:\programowaniefiles\convex-ts-bug> npx convex dev
✖ TypeScript typecheck via `tsc` failed.
To ignore failing typecheck, use `--typecheck=disable`.
convex/_generated/api.d.ts:27:15 - error TS2502: 'fullApi' is referenced directly or indirectly in its own type annotation.

27 declare const fullApi: ApiFromModules<{
                 ~~~~~~~
convex/_generated/api.d.ts:27:24 - error TS2615: Type of property 'actions' circularly references itself in mapped type '{ [Key in keyof { actions: FunctionReferencesInModule<typeof import("E:/programowaniefiles/convex-ts-bug/convex/actions")>; }]: ExpandModulesAndDirs<{ actions: FunctionReferencesInModule<typeof import("E:/programowaniefiles/convex-ts-bug/convex/actions")>; }[Key]>; }'.

27 declare const fullApi: ApiFromModules<{
                          ~~~~~~~~~~~~~~~~
28   actions: typeof actions;
   ~~~~~~~~~~~~~~~~~~~~~~~~~~
29 }>;
   ~~

convex/_generated/api.d.ts:34:22 - error TS2502: 'internal' is referenced directly or indirectly in its own type annotation.

34 export declare const internal: FilterApi<
                        ~~~~~~~~

convex/_generated/api.d.ts:35:10 - error TS2615: Type of property 'actions' circularly references itself in mapped type '{ actions: { getTasks: FunctionReference<"query", "internal", {}, { _id: Id<"tasks">; _creationTime: number; text: string; isCompleted: boolean; }[]>; invokeAction: FunctionReference<...> | ... 1 more ... | FunctionReference<...>; }; }'.

35   typeof fullApi,
            ~~~~~~~

convex/actions.ts:15:14 - error TS7022: 'invokeAction' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

15 export const invokeAction = action({
                ~~~~~~~~~~~~

convex/actions.ts:18:15 - error TS7022: 'tasks' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

18         const tasks = await ctx.runQuery(internal.actions.getTasks)
                 ~~~~~

convex/actions.ts:20:15 - error TS7022: 'texts' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

20         const texts = tasks.map(task => task.text)
                 ~~~~~

convex/actions.ts:20:33 - error TS7006: Parameter 'task' implicitly has an 'any' type.

20         const texts = tasks.map(task => task.text)
                                   ~~~~

Found 8 errors in 2 files.

Errors  Files
     4  convex/_generated/api.d.ts:27
     4  convex/actions.ts:15
xixixao commented 1 year ago

Yup, this is a known TS limitation. To get around it you need to type annotate the result value for either functions.

Thanks for reporting!

em-jones commented 1 year ago

Yup, this is a known TS limitation. To get around it you need to type annotate the result value for either functions.

Can I ask you for a code example? This instruction feels ambiguous, and I've attempted type annotating the return value of a handler and I've type-annotated an entire RegisteredQuery to no avail.

Would it hurt to add a TL;DR to the docs considering how well-known this issue is? I'd be happy to open an example PR on the docs if I can get it fixed in my codebase.

Thanks!

Willaiem commented 1 year ago

Yup, this is a known TS limitation. To get around it you need to type annotate the result value for either functions.

Can I ask you for a code example? This instruction feels ambiguous, and I've attempted type annotating the return value of a handler and I've type-annotated an entire RegisteredQuery to no avail.

Would it hurt to add a TL;DR to the docs considering how well-known this issue is? I'd be happy to open an example PR on the docs if I can get it fixed in my codebase.

Thanks!

The workaround that I found working is by extracting the type from the desired query/mutation/action.

import { action, internalQuery } from "./_generated/server";
import { internal } from "./_generated/api";

export const getTasks = internalQuery({
    args: {},
    handler: async (ctx) => {
        const tasks = await ctx.db.query('tasks').collect()

        return tasks
    }
})

type UnwrapConvex<T extends (...args: any[]) => Promise<any>> = Awaited<ReturnType<T>>

export const invokeAction = action({
    args: {},
    handler: async (ctx) => {
        const tasks = await ctx.runQuery(internal.actions.getTasks) as UnwrapConvex<typeof getTasks>
        // now the circular dependency error won't occur since we changed the source of the types to the query itself

        return tasks
    }
})

I wish I could just do this and call it a day:

const tasks = await ctx.runQuery(getTasks)
em-jones commented 1 year ago

I wonder if this is even what @xixixao was recommending...

To get around it you need to type annotate the result value for either functions. Convex overloaded the notion of a Function with their specification, so it's impossible to say if what you did was what they were suggesting/not. I do like your solution though, @Willaiem. I was trying to type annotate the actual convex function types with the convex generic types. It was really painful when I got to larger documents and typescript's lsp limitations(truncation of types) kicked in. Stuff like this really needs better documentation

xixixao commented 1 year ago

Thanks for the feedback and the cool helper from @Willaiem. We'll see what we can do about this issue and what to add to docs.

cc @thomasballinger