#Error

38 messages · Page 1 of 1 (latest)

cloud larkBOT
#

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

#

Ce message d’erreur TypeScript (L'instanciation de type est trop profonde et éventuellement infinie.ts(2589)) est un problème connu avec Convex, lié à la façon dont les types générés se référencent entre eux.

Les points clés tirés des sources :

  1. Cause la plus fréquente : ctx.runQuery / ctx.runMutation / ctx.runAction
    Si une fonction Convex (query/mutation/action) appelle une autre fonction via ctx.run* et retourne tout ou partie du résultat sans typer explicitement, cela crée une boucle de typage « circulaire » qui déclenche TS2589. [Circular inference; Actions doc]

    Exemple problématique (schéma) :

    export const myFunc = mutation({
      args: {},
      handler: async (ctx) => {
        const value = await ctx.runQuery(internal.some.func);
        return value; // ou value.aBoolean
      },
    });
    

    Même si l’erreur apparaît sur internal ou dans api.d.ts, la vraie source est souvent une de ces fonctions.

  2. Comment corriger : typer explicitement le retour
    Deux approches recommandées : [Circular inference; Very fun instance]

    • Typer le retour du handler :
      export const myFunc = mutation({
        args: {},
        handler: async (ctx): Promise<boolean> => {
          const value = await ctx.runQuery(internal.some.func);
          return value.aBoolean;
        },
      });
      
#
  • Ou typer la valeur retournée par runQuery sans utiliser les types Convex générés (Doc, Id, etc.) :

    export const myFunc = mutation({
      args: {},
      handler: async (ctx) => {
        const value: { aBoolean: boolean } =
          await ctx.runQuery(internal.some.func);
        return value.aBoolean;
      },
    });
    

    Important : utiliser Doc<"table"> ou Id<"table"> dans ce cast peut maintenir la boucle, car ces types viennent aussi des types générés. [Very fun instance]

  1. Chercher la source dans votre code
    Même si l’erreur est affichée sur import internal ou sur le type de internal, la cause est souvent ailleurs. Les membres du support recommandent de : [Type instantiation thread; Monorepo thread]

    • Rechercher toutes les occurrences de ctx.runQuery, ctx.runMutation, ctx.runAction dans convex/.
    • Pour chacune :
      • soit typer le handler (: Promise<...>),
      • soit typer la valeur retournée par run* avec un type simple (objet littéral, primitives), sans Doc/Id/types générés.
    • Éviter de retourner directement des objets complets provenant d’un autre run* ; si possible, ne retourner que les champs nécessaires avec un type explicite.
#
  1. Autres causes possibles vues dans les threads

    • v.any() dans des validateurs peut aussi déclencher ce genre d’erreur (par ex. v.record(v.string(), v.any())). Remplacer v.any() par un type plus précis a résolu le problème pour plusieurs personnes. [v.any issue]
    • Des dépendances circulaires entre fichiers Convex (par ex. autour de migrations ou d’auth) peuvent aussi causer des instanciations de type profondes. [Migrations thread; Auth api types]
  2. Option avancée : génération statique des types (beta)
    Il existe une option de « static code generation » qui coupe une grande partie de l’inférence et peut éliminer TS2589, au prix de devoir fournir des returns pour toutes les fonctions. [Static generation]


Pour vous aider plus précisément, il faudrait voir une fonction où vous utilisez internal.notifications.* (ou un ctx.run* qui appelle ces mutations) et qui déclenche l’erreur. Si vous collez un exemple de mutation/action/query qui utilise internal.notifications.createCommentNotification (ou similaire), je peux vous proposer la version typée qui devrait faire disparaître TS2589.

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

#

@tall scarab Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tall scarab
#

const data = await fetchQuery(api.organizations.dashboard.getStats, { organizationSlug }, { token });
L'instanciation de type est trop profonde et éventuellement infinie.ts(2589)
(alias) const api: {
notifications: {
getUnread: FunctionReference<"query", "public", {
limit?: number | undefined;
organizationId: Id<"organizations">;
}, {
_id: Id<"notifications">;
_creationTime: number;
status?: string | undefined;
platform?: "facebook" | "instagram" | undefined;
pageId?: string | undefined;
errorMessage?: string | undefined;
knowledgeBaseId?: Id<"knowledgeBases"> | undefined;
pageName?: string | undefined;
actionUrl?: string | undefined;
avatar?: string | undefined;
title: string;
type: "new_comment" | "new_dm" | "knowledge_base_update";
organizationId: Id<...>;
message: string;
isRead: boolean;
}[], string | undefined>;
... 4 more ...;
remove: FunctionReference<...>;
};
... 8 more ...;
templates: {
...;
};
}
import api
A utility for referencing Convex functions in your app's public API.

Usage:

const myFunctionReference = api.myModule.myFunction;

#

@cloud lark const data = await fetchQuery(api.organizations.dashboard.getStats, { organizationSlug }, { token });
L'instanciation de type est trop profonde et éventuellement infinie.ts(2589)
(alias) const api: {
notifications: {
getUnread: FunctionReference<"query", "public", {
limit?: number | undefined;
organizationId: Id<"organizations">;
}, {
_id: Id<"notifications">;
_creationTime: number;
status?: string | undefined;
platform?: "facebook" | "instagram" | undefined;
pageId?: string | undefined;
errorMessage?: string | undefined;
knowledgeBaseId?: Id<"knowledgeBases"> | undefined;
pageName?: string | undefined;
actionUrl?: string | undefined;
avatar?: string | undefined;
title: string;
type: "new_comment" | "new_dm" | "knowledge_base_update";
organizationId: Id<...>;
message: string;
isRead: boolean;
}[], string | undefined>;
... 4 more ...;
remove: FunctionReference<...>;
};
... 8 more ...;
templates: {
...;
};
}
import api
A utility for referencing Convex functions in your app's public API.

Usage:

const myFunctionReference = api.myModule.myFunction;

je modifie quoi?

cloud larkBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Le problème ne vient pas de cette ligne en elle‑même, mais d’une boucle d’inférence de types quelque part dans ton code Convex. L’erreur apparaît sur api / fetchQuery, mais la cause est presque toujours une fonction Convex qui utilise ctx.run* sans retour typé. [Circular inference; Actions doc]

Ce qu’il faut faire concrètement

  1. Dans le dossier convex/, cherche toutes les occurrences de :

    ctx.runQuery(
    ctx.runMutation(
    ctx.runAction(
    
  2. Pour chaque fonction qui utilise un de ces ctx.run* et retourne tout ou partie du résultat, fais l’un des deux :

    Option A – typer le handler

    export const someAction = action({
      args: { /* ... */ },
      handler: async (ctx, args): Promise<{ count: number }> => {
        const stats = await ctx.runQuery(
          internal.organizations.dashboard.getStats,
          { organizationSlug: args.organizationSlug }
        );
    
        // Ne retourne qu’un objet typé simple
        return { count: stats.count };
      },
    });
    

    L’important : le Promise<...> est un type simple, qui ne dépend pas de Doc, Id ou d’un type généré par Convex. [Very fun instance]

    Option B – typer le résultat de runQuery

    export const someAction = action({
      args: { /* ... */ },
      handler: async (ctx, args) => {
        const stats: { count: number; totalMessages: number } =
          await ctx.runQuery(
            internal.organizations.dashboard.getStats,
            { organizationSlug: args.organizationSlug }
          );
#

return stats; // OK car le type est un objet littéral
},
});


Ne pas utiliser ici `Doc<"table">` ou `Id<"table">`, ça garde la boucle d’inférence. [[Very fun instance](<https://discord.com/channels/1019350475847499849/1421722604556910725>)]

3. **Évite de retourner directement le résultat brut d’un `ctx.run*`** (par ex. `return await ctx.runQuery(...)`) sans type explicite.

4. Après les modifications, supprime éventuellement `convex/_generated`, relance `npx convex dev` et laisse les types se régénérer. [[TS server tips](<https://discord.com/channels/1019350475847499849/1427214311197118605>)]

---

Ta ligne côté Next :

```ts
const data = await fetchQuery(api.organizations.dashboard.getStats, { organizationSlug }, { token });

n’a probablement pas besoin d’être modifiée. Ce qu’il faut corriger, c’est la/les fonctions Convex qui appellent d’autres fonctions via ctx.run* et qui créent la boucle de types.

Si tu peux coller ici la définition de convex/organizations/dashboard.ts (surtout getStats et toute action/mutation qui l’appelle avec ctx.runQuery), je peux te montrer exactement quoi typer et où.

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

#

@tall scarab Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tall scarab
cloud larkBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Sorry @tall scarab, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist:

Perhaps try again in 15mins?

tall scarab
#

@cloud lark export const processDocument = action({
args: {
knowledgeBaseId: v.id("knowledgeBases"),
content: v.string(),
},
handler: async (ctx, { knowledgeBaseId, content }): Promise<{ success: boolean; chunksCreated: number }> => {
try {
const chunks = chunkText(content, 500, 50);
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!openaiApiKey) throw new Error("OPENAI_API_KEY not configured");

  for (let i = 0; i < chunks.length; i++) {
    const chunk = chunks[i];
    // ... fetch OpenAI embedding ...
    const embedding = data.data[0].embedding;

    // TS2589 ERROR ICI ↓
    await ctx.runMutation(internal.ai.knowledgeBaseInternal.insertChunk, {
      knowledgeBaseId,
      chunkIndex: i,
      content: chunk,
      embedding,
      metadata: { tokenCount: chunk.split(/\s+/).length, position: i, totalChunks: chunks.length },
    });
  }

  // TS2589 ERROR ICI AUSSI ↓
  await ctx.runMutation(internal.ai.knowledgeBaseInternal.updateStatus, {
    knowledgeBaseId,
    status: "COMPLETED",
  });

  return { success: true, chunksCreated: chunks.length };
} catch (error) {
  await ctx.runMutation(internal.ai.knowledgeBaseInternal.updateStatus, {
    knowledgeBaseId, status: "ERROR", errorMessage: error instanceof Error ? error.message : "Unknown error",
  });
  throw error;
}

},
});

insertChunk et updateStatus sont dans un fichier séparé (knowledgeBaseInternal.ts)
Le handler a déjà un type de retour explicite Promise<{ success: boolean; chunksCreated: number }>
L'erreur persiste même avec typage explicite du résultat (const _result: string = await ctx.runMutation(...))

cloud larkBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

L’erreur TS2589 dans ton cas ne vient pas du typage de processDocument lui‑même (il est déjà correct), mais du fait que tu appelles des mutations internes (insertChunk, updateStatus) qui elles‑mêmes s’appuient sur des types Convex générés, ce qui maintient une boucle d’inférence, même si tu tapes le résultat de runMutation. C’est exactement ce que les threads Discord décrivent : même avec un Promise<...> explicite, si le type traverse encore les types générés (Doc, Id, etc.), la boucle reste. [Very fun instance; Deep instantiation]

Voici ce que tu peux faire, en restant dans ce que les sources recommandent.


1. Vérifier insertChunk et updateStatus

Regarde dans knowledgeBaseInternal.ts :

  • Si ces mutations retournent quelque chose (par ex. un Id, un Doc, ou un objet contenant des Id/Doc), et que ce retour est typé avec des types Convex (Id<"…">, Doc<"…">, etc.), alors même si tu ignores le résultat côté processDocument, TypeScript doit quand même instancier ces types pour générer internal.ai.knowledgeBaseInternal.insertChunk, ce qui peut contribuer à la profondeur excessive. [Very fun instance]

Solution recommandée par les threads : pour les mutations internes purement “effet de bord” comme celles‑ci, fais‑les retourner void (ou rien) et tape leur handler en conséquence, sans returns complexe.

Exemple :

#
// knowledgeBaseInternal.ts
export const insertChunk = internalMutation({
  args: {
    knowledgeBaseId: v.id("knowledgeBases"),
    chunkIndex: v.number(),
    content: v.string(),
    embedding: v.array(v.number()),
    metadata: v.object({
      tokenCount: v.number(),
      position: v.number(),
      totalChunks: v.number(),
    }),
  },
  handler: async (ctx, args): Promise<void> => {
    await ctx.db.insert("knowledgeBaseChunks", {
      knowledgeBaseId: args.knowledgeBaseId,
      chunkIndex: args.chunkIndex,
      content: args.content,
      embedding: args.embedding,
      metadata: args.metadata,
    });
    // ne rien retourner
  },
});

export const updateStatus = internalMutation({
  args: {
    knowledgeBaseId: v.id("knowledgeBases"),
    status: v.string(),
    errorMessage: v.optional(v.string()),
  },
  handler: async (ctx, args): Promise<void> => {
    await ctx.db.patch(args.knowledgeBaseId, {
      status: args.status,
      errorMessage: args.errorMessage,
    });
    // ne rien retourner
  },
});

Ici, les handlers retournent Promise<void> et ne renvoient aucun Doc/Id/objet complexe, ce qui coupe une partie de la chaîne d’inférence. [Very fun instance]


2. Garder processDocument simple et sans types Convex dans son retour

Ta définition actuelle est déjà dans l’esprit des recommandations :

#
export const processDocument = action({
  args: {
    knowledgeBaseId: v.id("knowledgeBases"),
    content: v.string(),
  },
  handler: async (
    ctx,
    { knowledgeBaseId, content }
  ): Promise<{ success: boolean; chunksCreated: number }> => {
    // ...
    await ctx.runMutation(internal.ai.knowledgeBaseInternal.insertChunk, { ... });
    await ctx.runMutation(internal.ai.knowledgeBaseInternal.updateStatus, { ... });
    return { success: true, chunksCreated: chunks.length };
  },
});

Ici, le type de retour est un simple objet { success: boolean; chunksCreated: number }, sans Doc/Id, ce qui est conforme aux conseils des docs et des threads. [Actions doc]

Tu n’as pas besoin de typer le résultat de runMutation (et de toute façon, les mutations retournent souvent void ou un Id que tu n’utilises pas ici).


3. Autres points à vérifier (vus dans les threads)

Si l’erreur persiste après avoir simplifié insertChunk et updateStatus :

  1. Cherche d’autres ctx.run* dans ton projet Convex (toutes les actions/mutations/queries) et applique la même logique :

    • handler typé (Promise<...> simple),
    • ou résultat de run* typé avec un objet littéral/primitives,
    • éviter de retourner directement des Doc/Id provenant d’un autre run*. [Deep instantiation]
  2. Évite v.any() dans les validateurs (par ex. v.record(v.string(), v.any())) qui a été signalé comme déclencheur de TS2589 dans plusieurs cas. [v.any issue]

#
  1. Si tu as des fichiers “migrations” ou des composants qui importent à la fois DataModel et api/internal, vérifie qu’il n’y a pas de dépendance circulaire entre ces fichiers. [Migrations thread]

En résumé : dans ton exemple, tu n’as probablement rien à changer dans processDocument lui‑même. La clé est de rendre insertChunk et updateStatus (et toute autre mutation interne appelée via ctx.runMutation) aussi simples que possible côté types : handlers Promise<void> ou avec un petit objet littéral, sans renvoyer de types Convex générés. Cela correspond exactement aux solutions proposées dans les threads et la doc sur l’inférence circulaire.

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

#

@tall scarab Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tall scarab
#

Créé knowledgeBaseInternal.ts - fichier séparé pour les internal mutations/queries avec Promise<void> explicite
Utilisé require() au lieu de import pour internal - cela évite que TypeScript évalue les types profonds au moment de la compilation

// Workaround pour TS2589
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { internal } = require("../_generated/api");

c bon ? @cloud lark

cloud larkBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Globalement : ça peut “marcher”, mais ce n’est pas la voie recommandée par les ressources, et c’est plutôt un hack de dernier recours.

1. Séparer knowledgeBaseInternal.ts + Promise<void>

Ça, c’est dans le bon sens :

  • Mettre les internal mutations/queries dans un fichier séparé.
  • Leur faire retourner Promise<void> (ou un petit objet littéral sans Doc/Id/types générés).

C’est exactement ce que les threads recommandent pour casser les boucles d’inférence. [Very fun instance; Circular inference]

2. require("../_generated/api") comme workaround

Des gens ont effectivement utilisé des require / imports dynamiques pour “casser la chaîne” de typage quand ils n’arrivaient pas à résoudre TS2589 autrement, et ils qualifient ça eux‑mêmes de solution “super hacky”. [Excessive instantiation]

Les points à garder en tête :

  • Ça évite à TypeScript de suivre les types profonds de internal, donc ça peut faire disparaître l’erreur.
  • Mais tu perds une partie de l’inférence statique (TS voit moins bien les types de internal), et ce n’est pas un pattern officiellement recommandé dans la doc Convex.
  • Les mainteneurs et la doc insistent plutôt sur :
    • typer les handlers / résultats de ctx.run*,
    • éviter Doc/Id/v.any() dans les chemins qui passent par ctx.run*,
    • éventuellement utiliser la génération statique des types si vraiment nécessaire. [Static generation; v.any issue]

Donc :

#
  • Oui, comme workaround ponctuel, c’est “bon” dans le sens où ça ne casse pas Convex côté runtime et ça peut débloquer ton projet.
  • Non, ce n’est pas idéal à long terme : si possible, essaie d’abord de résoudre TS2589 en nettoyant les ctx.run* et les types de retour, et ne garder require que si tu n’as vraiment pas d’autre option.

Si tu veux, tu peux coller le contenu de knowledgeBaseInternal.ts et je peux te dire si, en théorie, tu devrais pouvoir enlever le require et revenir à un import { internal } from "../_generated/api"; sans TS2589.

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

#

@tall scarab Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tall scarab
#

import { v } from "convex/values";
import { internalMutation, internalQuery } from "../_generated/server";

// Type pour metadata (doit correspondre au schema - string ou number)
const metadataValidator = v.optional(v.record(v.string(), v.union(v.string(), v.number())));

// ============================================
// INTERNAL MUTATIONS (séparées pour éviter TS2589)
// ============================================

/**

  • Mettre à jour le statut d'une knowledge base
  • Note: retourne void pour éviter TS2589 (boucle d'inférence de type)
    */
    export const updateStatus = internalMutation({
    args: {
    knowledgeBaseId: v.id("knowledgeBases"),
    status: v.union(
    v.literal("PROCESSING"),
    v.literal("COMPLETED"),
    v.literal("ERROR")
    ),
    errorMessage: v.optional(v.string()),
    },
    handler: async (ctx, { knowledgeBaseId, status, errorMessage }): Promise<void> => {
    await ctx.db.patch(knowledgeBaseId, {
    status,
    errorMessage,
    isActive: status === "COMPLETED",
    });
    },
    });

/**

  • Insérer un chunk avec son embedding
  • Note: retourne void pour éviter TS2589 (boucle d'inférence de type)
    */
    export const insertChunk = internalMutation({
    args: {
    knowledgeBaseId: v.id("knowledgeBases"),
    chunkIndex: v.number(),
    content: v.string(),
    embedding: v.array(v.float64()),
    metadata: metadataValidator,
    },
    handler: async (ctx, args): Promise<void> => {
    await ctx.db.insert("knowledgeBaseChunks", args);
    // Ne rien retourner pour éviter la boucle d'inférence de type
    },
    });

/**

  • Récupère un chunk par ID (internal)
    */
    export const getChunk = internalQuery({
    args: { chunkId: v.id("knowledgeBaseChunks") },
    handler: async (ctx, { chunkId }) => {
    return await ctx.db.get(chunkId);
    },
    });

/**

  • Internal query pour récupérer les KB actives
    */
    export const getActiveKnowledgeBases = internalQuery({
    args: {
    organizationId: v.id("organizations"),
    channels: v.optional(v.array(v.string())),
    },
    handler: async (ctx, { organizationId, channels }) => {
    const kbs = await ctx.db
    .query("knowledgeBases")
    .withIndex("by_active", (q) =>
    q.eq("organizationId", organizationId).eq("isActive", true)
    )
    .collect();

    // Filtrer par channel si spécifié
    if (channels && channels.length > 0) {
    return kbs.filter(
    (kb) =>
    kb.channels.includes("ALL") ||
    kb.channels.some((c) => channels.includes(c))
    );
    }

    return kbs;
    },
    });
    @cloud lark

cloud larkBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Tu es dans la bonne direction, mais il reste deux points importants par rapport à ce qu’on sait de TS2589 avec Convex.

1. Les mutations updateStatus et insertChunk

Elles sont bien structurées pour éviter les boucles d’inférence :

  • internalMutation
  • handler: Promise<void>
  • pas de Doc / Id dans le type de retour
  • pas de returns avec un validateur complexe

C’est exactement ce que les threads recommandent pour des mutations internes “effet de bord”. [Very fun instance]

Donc ces deux‑là sont OK.

2. Les internalQuery (getChunk, getActiveKnowledgeBases)

C’est plus souvent les actions qui appellent des queries/mutations via ctx.run* qui déclenchent TS2589, pas les queries elles‑mêmes. Mais deux remarques utiles :

  1. getChunk :

    export const getChunk = internalQuery({
      args: { chunkId: v.id("knowledgeBaseChunks") },
      handler: async (ctx, { chunkId }) => {
        return await ctx.db.get(chunkId);
      },
    });
    

    Ici tu laisses Convex inférer le type de retour (un Doc<"knowledgeBaseChunks"> | null).
    Ce n’est pas un problème en soi, sauf si tu appelles cette query via ctx.runQuery dans une action/mutation et que tu retournes tout ou partie du résultat sans typer explicitement. C’est là que la boucle d’inférence se crée. [Circular inference]

    Si tu as une action qui fait :

    const chunk = await ctx.runQuery(internal.ai.knowledgeBaseInternal.getChunk, { chunkId });
    return chunk; // ou chunk?.quelqueChose
    

    alors il faut soit :

#
   handler: async (ctx, args): Promise<{ content: string } | null> => {
     const chunk = await ctx.runQuery(internal.ai.knowledgeBaseInternal.getChunk, args);
     if (!chunk) return null;
     return { content: chunk.content };
   },

ou typer le résultat de runQuery avec un type simple (objet littéral), sans Doc. [Deep instantiation]

  1. getActiveKnowledgeBases :

    export const getActiveKnowledgeBases = internalQuery({
      args: {
        organizationId: v.id("organizations"),
        channels: v.optional(v.array(v.string())),
      },
      handler: async (ctx, { organizationId, channels }) => {
        const kbs = await ctx.db
          .query("knowledgeBases")
          .withIndex("by_active", (q) =>
            q.eq("organizationId", organizationId).eq("isActive", true)
          )
          .collect();
    
        if (channels && channels.length > 0) {
          return kbs.filter(
            (kb) =>
              kb.channels.includes("ALL") ||
              kb.channels.some((c) => channels.includes(c))
          );
        }
    
        return kbs;
      },
    });
    

    Même logique : le handler retourne implicitement Doc<"knowledgeBases">[].
    Ce n’est pas un problème tant que tu n’as pas une action/mutation qui fait :

    const kbs = await ctx.runQuery(internal.ai.knowledgeBaseInternal.getActiveKnowledgeBases, { ... });
    return kbs; // ou un sous-ensemble, sans typage explicite
    

    Dans ce cas, il faut :

    handler: async (ctx, args): Promise<{ id: string; name: string }[]> => {
      const kbs = await ctx.runQuery(internal.ai.knowledgeBaseInternal.getActiveKnowledgeBases, args);
      return kbs.map(kb => ({ id: kb._id, name: kb.name }));
    },
    
#

ou typer kbs avec un type littéral simple. [Actions doc]

3. Le validateur metadataValidator

Tu as :

const metadataValidator = v.optional(
  v.record(v.string(), v.union(v.string(), v.number()))
);

Les sources signalent que v.any() dans des record a déjà déclenché des TS2589, mais ici tu n’utilises pas v.any(), donc tu es dans un cas plus “safe”. [v.any issue]

Si tu continues à avoir TS2589 malgré tout :

  • vérifie toutes les actions/mutations qui appellent ces internal queries/mutations via ctx.run* et applique systématiquement :
    • handler typé (Promise<...> simple), ou
    • résultat de run* typé avec un objet littéral / primitives, sans Doc/Id.

C’est exactement la stratégie recommandée dans la doc et les threads pour casser les boucles d’inférence. [Circular inference; Deep instantiation]


En résumé :

  • Ton knowledgeBaseInternal.ts est globalement bien structuré pour éviter TS2589.
  • Le point critique reste les actions/mutations qui appellent ces fonctions via ctx.run* : c’est là qu’il faut vérifier et typer explicitement le retour, comme dans les exemples ci‑dessus.

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

#

@tall scarab Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tall scarab
#

'ai testé les deux approches:
Fichier séparé knowledgeBaseInternal.ts avec Promise<void> pour les mutations et Promise<Doc<"...">> pour les queries ✅
Typage explicite des résultats de ctx.runMutation/ctx.runQuery dans l'action processDocument et searchSimilar ❌ L'erreur TS2589 persiste même avec le typage explicite
Le seul workaround qui fonctionne est:

// eslint-disable-next-line @typescript-eslint/no-require-imports
const { internal } = require("../_generated/api");
Même en typant explicitement comme const _result: void = await ctx.runMutation(...), l'erreur TS2589 apparaît sur la ligne du runMutation. TypeScript semble évaluer le type de internal.ai.knowledgeBaseInternal.insertChunk avant même d'appliquer le typage du résultat. Est-ce que c'est un bug connu de Convex? Y a-t-il une autre solution que le require()? @cloud lark

cloud larkBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Je ne vois rien dans les sources qui mentionne explicitement l’usage de require("../_generated/api") comme workaround “officiel”, ni un bug documenté où TS2589 persiste malgré :

  • mutations internes Promise<void> dans un fichier séparé, et
  • typage explicite du résultat de ctx.runMutation/ctx.runQuery dans l’action.

Donc je ne peux pas confirmer que c’est un bug reconnu côté Convex à partir des infos disponibles.

Ce que les docs + threads disent clairement :

  1. Cause principale documentée
    TS2589 vient quasi toujours d’une inférence circulaire via ctx.runQuery / ctx.runMutation / ctx.runAction quand le type de retour de la fonction appelante dépend (directement ou indirectement) des types générés de la fonction appelée. [Actions circular; Deep instantiation]

    Même si l’erreur apparaît “sur” internal ou sur la ligne du runMutation, la cause peut être ailleurs (une autre action, un autre ctx.run*). [Type instantiation thread]

  2. Les deux solutions “officielles” (que tu as déjà testées) [Actions circular; Very fun instance]

    • Typage explicite du handler (handler: async (…) : Promise<…> => { … }) avec un type de retour simple (sans Doc/Id/types générés).
    • Typage explicite du résultat de ctx.run* avec un type littéral simple (objet/primitives), toujours sans Doc/Id.
#

Les membres du support insistent aussi sur le fait que returns (le validateur) ne coupe pas la boucle : c’est bien le type du handler ou de la variable issue de run* qui compte. [Very fun instance]

  1. Autres sources possibles de TS2589
    D’après les threads, il peut aussi y avoir :

    • Des dépendances circulaires entre fichiers (par ex. autour de migrations ou d’auth) qui font que les types générés se référencent en boucle. [Migrations thread; Auth api types]
    • Des validateurs trop génériques comme v.any() dans des record, qui ont déjà déclenché TS2589 chez d’autres. [v.any issue]
    • Des actions “gros paquets” qui mélangent beaucoup de logique et de types générés, où la seule façon de s’en sortir a été de refactorer en couches (services purs, internal API, public API) pour réduire les chemins d’inférence. [Excessive instantiation]
#
  1. Workarounds vus dans la communauté

    • Certains ont effectivement parlé de “dynamic require” / imports dynamiques pour casser la chaîne de typage, mais c’est décrit comme “super hacky” et pas comme une solution recommandée. [Excessive instantiation]
    • D’autres ont fini par activer la génération statique des types (beta), qui coupe une grande partie de l’inférence (au prix de devoir fournir des returns partout) et élimine TS2589 dans de gros projets. [Static generation]
  2. Ce que ça implique pour ton cas

À partir des sources, je peux dire :

  • Le pattern require("../_generated/api") n’est pas documenté comme “bugfix” officiel, mais il est cohérent avec ce que les gens décrivent : en empêchant TS de suivre les types profonds de internal, tu casses la boucle d’inférence, donc TS2589 disparaît.
  • Si, malgré :
    • knowledgeBaseInternal.ts séparé,
    • mutations internes Promise<void>,
    • typage explicite dans processDocument / searchSimilar,
    • et nettoyage des autres ctx.run* potentiellement en cause,
      tu as encore TS2589, alors tu es dans un cas limite que les docs ne couvrent pas en détail.

Les seules alternatives mentionnées dans les sources, en dehors de ton require, sont :

#
  • Pousser plus loin le refactor pour réduire les chemins d’inférence (séparer davantage la logique métier des fonctions Convex, éviter que des actions retournent des structures proches des Doc, etc.). [Excessive instantiation]
  • Essayer la génération statique des types si tu es prêt à sacrifier une partie de l’inférence automatique. [Static generation]

Je n’ai pas, dans les sources, de confirmation explicite “oui, c’est un bug connu, utilisez require” ni une autre solution miracle documentée. Si tu veux éviter require, la prochaine étape logique (d’après les threads) serait soit :

  • partager un repro minimal sur Discord pour que l’équipe puisse confirmer si c’est bien un bug,
  • soit tester la génération statique des types pour voir si elle supprime TS2589 dans ton projet.

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