You write a type that walks through nested objects, building dot-separated paths for autocomplete. It works beautifully on a small test object. Then you point it at a real API payload, and the editor freezes. Eventually, TypeScript spits out error TS2589: Type instantiation is excessively deep and possibly infinite.
This message does not mean your code contains an infinite loop in the traditional sense. It means the compiler gave up. The type you asked it to compute was either genuinely unbounded, or finite but so large that evaluating it would exhaust TypeScript’s internal limits. When this happens, the compiler halts before it hangs your IDE.
When TS2589 Appears
Recursive types are the most common culprit. TypeScript evaluates types eagerly, and if a utility type keeps calling itself—especially through conditional logic—the computation stack grows quickly. You will typically hit this wall in a few specific scenarios:
- Recursive conditional types that repeatedly destructure a tuple, object, or string template until reaching a base case
- Deeply nested object path generators, which turn structures like
{ user: { address: { street: string } } }into unions of string literals such as"user" | "user.address" | "user.address.street" - Template literal types that parse strings character by character or token by token
- Mapped types iterated over objects with dozens of keys and multiple levels
- Conditional types that distribute over large unions, silently multiplying the workload across every member
The nested path example is especially seductive. Form libraries and state-management tools love offering typed paths so you get autocomplete for field names. On a shallow object, generating every legal dot-path as a string union is trivial. On a deep or wide object, that union explodes. TypeScript must hold every permutation in working memory at once. At some depth, the compiler notices the work is outpacing its budget and pulls the emergency brake.
Fix One: Add a Hard Depth Limit
The most direct way to solve TS2589 is to stop pretending your type can recurse forever. Introduce a depth counter that acts as a circuit breaker.
In practice, this means adding a numeric generic parameter—often represented as a tuple whose length counts down—that decreases each time the type recurses. When the counter hits zero, the type returns a broad fallback such as string instead of drilling further. Users still get precise autocomplete for the first four or five levels, which covers the vast majority of real-world objects. Beyond that, the compiler simply widens the type and moves on.
This approach does not make your utility type less correct in any meaningful way. It makes it bounded. A type system that crashes the compiler is not more useful than one that concedes gracefully after a sensible depth.
Fix Two: Validate One Path at a Time
If generating every possible path up front is too expensive, change the contract. Instead of producing a massive union of all valid strings, write a type that checks whether one specific string is a valid path.
Think of the difference between creating a dictionary of every English word versus checking if a single word is spelled correctly. The former is a huge data structure; the latter is a lightweight scan. In TypeScript terms, rather than exporting a Paths<T> utility that yields "user.address.street" | "user.settings.theme" | ..., you export something like IsValidPath<T, "user.address.street">. The compiler only evaluates the path you actually pass in.
This shift changes how you design APIs. Your function signatures might accept a string and then use a generic constraint to verify it against the object shape. The IDE still complains if the developer types a bad path, but the compiler never has to materialize the full set of legal paths during type-checking. For large objects, the performance difference is dramatic.
Quick Tactics That Keep You Moving
Beyond the two structural fixes, a few smaller habits can keep recursive types from crossing the line:
Огортайте параметри типів у кортежі, щоб заблокувати розподіл. Необгорнутий параметр типу в умовному типі, наприклад
T extends Foo ? Bar : Baz, розподіляє перевірку по кожному члену, якщоTє об'єднанням (union). Якщо це об'єднання має п'ятдесят членів, TypeScript виконує п'ятдесят окремих ініціалізацій. Запис[T] extends [Foo] ? Bar : Bazобчислює умову один раз для всього об'єднання. Використовуйте це щоразу, коли вам насправді не потрібно, щоб тип проходив по кожному члену об'єднання окремо.Зменшуйте вхідні дані під час налагодження. Коли з'являється помилка TS2589, замініть тип об'єкта з продакшну на крихітну заглушку з двома властивостями та одним рівнем вкладеності. Якщо помилка зникає, ви підтвердили, що проблемою є глибина або кардинальність, а не синтаксична помилка. Це вбереже вас від переписування логіки, яка насправді була структурно правильною.
Пом'якшуйте типи публічного API. Внутрішньо вам може знадобитися хірургічна точність. Зовні ж досконалість іноді коштує дорожче, ніж приносить користі. Якщо трохи ширший тип для автодоповнення запобігає двосекундній затримці в редакторі, така компромісна угода зазвичай того варта. Ви можете поєднати менш суворий тип із валідатором під час виконання (runtime validator), щоб виявляти некоректні шляхи під час тестування.
Чому TypeScript встановлює цю межу
TypeScript не може вирішити проблему зупинки. Він не знає, чи завершиться ваш рекурсивний тип зрештою, чи буде розкручуватися нескінченно. Замість того, щоб ризикувати нескінченним циклом всередині компілятора, він встановлює консервативний ліміт. Іноді цей ліміт зупиняє тип, який міг би завершитися, якби мав достатньо часу. TS2589 — це визнання компілятора, що він воліє перестрахуватися, ніж шкодувати.
Дотримання цього ліміту є частиною написання типів промислового рівня. Визначення типу — це код, який виконується в компіляторі, а дорогий код має реальні наслідки. Повільне автодоповнення шкодить швидкості розробки так само, як повільний код під час виконання шкодить користувацькому досвіду.
Головний висновок
TS2589 — це не сигнал про те, що ви поганий програміст систем типів. Це сигнал про те, що ваш тип виконує занадто багато роботи одночасно. Обмежуйте рекурсію, використовуйте ліниву валідацію та захищайтеся від зайвого розподілу. Мета складних типів не в тому, щоб довести кожну можливу істину під час компіляції; вона в тому, щоб надати вашій команді швидкі та надійні інструменти. Тип, який компілюється за мілісекунди та охоплює дев'яносто п'ять відсотків випадків, набагато цінніший за той, що є теоретично ідеальним, але призводить до збою сервера мови.
