#Error
38 messages · Page 1 of 1 (latest)
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 :
-
Cause la plus fréquente :
ctx.runQuery / ctx.runMutation / ctx.runAction
Si une fonction Convex (query/mutation/action) appelle une autre fonction viactx.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
internalou dansapi.d.ts, la vraie source est souvent une de ces fonctions. -
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; }, });
- Typer le retour du handler :
-
Ou typer la valeur retournée par
runQuerysans 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">ouId<"table">dans ce cast peut maintenir la boucle, car ces types viennent aussi des types générés. [Very fun instance]
-
Chercher la source dans votre code
Même si l’erreur est affichée surimport internalou sur le type deinternal, 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.runActiondansconvex/. - Pour chacune :
- soit typer le handler (
: Promise<...>), - soit typer la valeur retournée par
run*avec un type simple (objet littéral, primitives), sansDoc/Id/types générés.
- soit typer le handler (
- É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.
- Rechercher toutes les occurrences de
-
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())). Remplacerv.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]
-
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 desreturnspour 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.
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?
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
-
Dans le dossier
convex/, cherche toutes les occurrences de :ctx.runQuery( ctx.runMutation( ctx.runAction( -
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 deDoc,Idou d’un type généré par Convex. [Very fun instance]Option B – typer le résultat de
runQueryexport 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.
@cloud lark
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?
@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(...))
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, unDoc, ou un objet contenant desId/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érerinternal.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 :
-
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/Idprovenant d’un autrerun*. [Deep instantiation]
- handler typé (
-
É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]
- Si tu as des fichiers “migrations” ou des composants qui importent à la fois
DataModeletapi/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.
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
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 sansDoc/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 parctx.run*, - éventuellement utiliser la génération statique des types si vraiment nécessaire. [Static generation; v.any issue]
- typer les handlers / résultats de
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 garderrequireque 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.
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
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 :
internalMutationhandler: Promise<void>- pas de
Doc/Iddans le type de retour - pas de
returnsavec 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 :
-
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 viactx.runQuerydans 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?.quelqueChosealors 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]
-
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 expliciteDans 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, sansDoc/Id.
- handler typé (
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.tsest 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.
'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
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.runQuerydans 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 :
-
Cause principale documentée
TS2589 vient quasi toujours d’une inférence circulaire viactx.runQuery / ctx.runMutation / ctx.runActionquand 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”
internalou sur la ligne durunMutation, la cause peut être ailleurs (une autre action, un autrectx.run*). [Type instantiation thread] -
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 (sansDoc/Id/types générés). - Typage explicite du résultat de
ctx.run*avec un type littéral simple (objet/primitives), toujours sansDoc/Id.
- Typage explicite du handler (
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]
-
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 desrecord, 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]
-
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
returnspartout) et élimine TS2589 dans de gros projets. [Static generation]
-
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 deinternal, tu casses la boucle d’inférence, donc TS2589 disparaît. - Si, malgré :
knowledgeBaseInternal.tssé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.