Scrivi un tipo che attraversa oggetti annidati, costruendo percorsi separati da punti per l'autocomplete. Funziona perfettamente su un piccolo oggetto di test. Poi lo punti verso un payload API reale e l'editor si blocca. Alla fine, TypeScript restituisce l'errore TS2589: Type instantiation is excessively deep and possibly infinite.

Questo messaggio non significa che il tuo codice contenga un loop infinito nel senso tradizionale. Significa che il compilatore si è arreso. Il tipo che gli hai chiesto di calcolare era o genuinamente illimitato, o finito ma così grande che la sua valutazione avrebbe esaurito i limiti interni di TypeScript. Quando ciò accade, il compilatore si ferma prima di bloccare il tuo IDE.

Quando appare TS2589

I tipi ricorsivi sono il colpevole più comune. TypeScript valuta i tipi in modo "eager", e se un tipo utility continua a chiamare se stesso — specialmente attraverso una logica condizionale — lo stack di calcolo cresce rapidamente. In genere, ti imbatterai in questo ostacolo in alcuni scenari specifici:

  • Tipi condizionali ricorsivi che destrutturano ripetutamente una tupla, un oggetto o un template string fino a raggiungere un caso base
  • Generatori di percorsi di oggetti profondamente annidati, che trasformano strutture come { user: { address: { street: string } } } in unioni di letterali di stringa come "user" | "user.address" | "user.address.street"
  • Tipi template literal che analizzano le stringhe carattere per carattere o token per token
  • Mapped types iterati su oggetti con decine di chiavi e molteplici livelli
  • Tipi condizionali che si distribuiscono su grandi unioni, moltiplicando silenziosamente il carico di lavoro su ogni membro

L'esempio del percorso annidato è particolarmente seducente. Le librerie per i form e gli strumenti di gestione dello stato amano offrire percorsi tipizzati per consentire l'autocomplete dei nomi dei campi. Su un oggetto superficiale, generare ogni percorso con il punto come unione di stringhe è banale. Su un oggetto profondo o ampio, quell'unione esplode. TypeScript deve mantenere ogni permutazione nella memoria di lavoro contemporaneamente. A una certa profondità, il compilatore nota che il lavoro sta superando il suo budget e tira il freno d'emergenza.

Soluzione 1: Aggiungi un limite di profondità rigido

Il modo più diretto per risolvere TS2589 è smettere di fingere che il tuo tipo possa ricorsare all'infinito. Introduci un contatore di profondità che agisca come un interruttore di sicurezza (circuit breaker).

In pratica, questo significa aggiungere un parametro generico numerico — spesso rappresentato come una tupla la cui lunghezza decresce — che diminuisce ogni volta che il tipo ricorre. Quando il contatore arriva a zero, il tipo restituisce un fallback generico come string invece di scavare ulteriormente. Gli utenti ottengono comunque un autocomplete preciso per i primi quattro o cinque livelli, il che copre la stragrande maggioranza degli oggetti del mondo reale. Oltre questo limite, il compilatore si limita ad ampliare il tipo e prosegue.

Questo approccio non rende il tuo tipo utility meno corretto in alcun modo significativo. Lo rende limitato. Un sistema di tipi che manda in crash il compilatore non è più utile di uno che si arrende con grazia dopo una profondità ragionevole.

Soluzione 2: Valida un percorso alla volta

Se generare in anticipo ogni possibile percorso è troppo costoso, cambia il contratto. Invece di produrre una massiccia unione di tutte le stringhe valide, scrivi un tipo che verifichi se una stringa specifica è un percorso valido.

Pensa alla differenza tra creare un dizionario di ogni parola inglese e controllare se una singola parola è scritta correttamente. Il primo è una struttura dati enorme; il secondo è una scansione leggera. In termini di TypeScript, invece di esportare un'utility Paths<T> che restituisce "user.address.street" | "user.settings.theme" | ..., esporti qualcosa come IsValidPath<T, "user.address.street">. Il compilatore valuta solo il percorso che passi effettivamente.

Questo cambiamento modifica il modo in cui progetti le API. Le firme delle tue funzioni potrebbero accettare una stringa e poi usare un vincolo generico per verificarla rispetto alla forma dell'oggetto. L'IDE continuerà a segnalare un errore se lo sviluppatore digita un percorso errato, ma il compilatore non dovrà mai materializzare l'intero set di percorsi validi durante il controllo dei tipi. Per gli oggetti di grandi dimensioni, la differenza di prestazioni è drastica.

Tattiche rapide per continuare a procedere

Oltre alle due soluzioni strutturali, alcune piccole abitudini possono impedire ai tipi ricorsivi di superare il limite:

  • Wrap type parameters in tuples to block distribution. A naked type parameter in a conditional, like T extends Foo ? Bar : Baz, distributes the check across every member when T is a union. If that union has fifty members, TypeScript performs fifty separate instantiations. Writing [T] extends [Foo] ? Bar : Baz evaluates the conditional once against the whole union. Use this whenever you do not actually need the type to map over each union member individually.

  • Shrink your inputs while debugging. When TS2589 appears, swap your production object type for a tiny stub with two properties and one level of nesting. If the error vanishes, you have confirmed that depth or cardinality is the issue, not a syntax mistake. This saves you from rewriting logic that was actually fine structurally.

  • Soften public-facing API types. Internally, you might need surgical precision. Externally, perfection sometimes costs more than it pays. If a slightly wider autocomplete type prevents a two-second lag in the editor, the trade is usually worth it. You can pair the looser type with a runtime validator to catch bad paths in testing.

Why TypeScript Enforces This Boundary

TypeScript cannot solve the halting problem. It does not know whether your recursive type will eventually terminate or spiral forever. Rather than risk an infinite loop inside the compiler, it enforces a conservative cutoff. Sometimes that cutoff catches a type that would have finished, given enough time. TS2589 is the compiler admitting it would rather be safe than sorry.

Respecting that limit is part of writing production-grade types. A type definition is code that runs in the compiler, and expensive code has real consequences. Slow autocomplete hurts developer velocity just as much as slow runtime code hurts user experience.

The Real Takeaway

TS2589 is not a signal that you are a bad type-system programmer. It is a signal that your type is doing too much work at once. Cap your recursion, validate lazily, and guard against unnecessary distribution. The goal of advanced types is not to prove every possible truth at compile time; it is to give your team fast, reliable tooling. A type that compiles in milliseconds and covers ninety-five percent of cases is far more valuable than one that is theoretically perfect but crashes the language server.