Type Instantiation Is Excessively Deep and Possibly Infinite
TypeScript gave up. Here is why recursive conditional types hit a wall, and how to restructure so they do not.
The short answer
error TS2589: Type instantiation is excessively deep and possibly infinite.
TypeScript's type checker has a recursion limit, historically around 50 levels for type instantiation and 1000 for some operations. A recursive conditional or mapped type exceeded it, so the checker stopped and returned any.
Three fixes in order of preference:
- Add a depth counter to your recursive type so it terminates explicitly
- Simplify the type to something non recursive that covers your real cases
- Break the recursion with an explicit interface at the point of nesting
Never fix it by making the type any, which is what the error already did and which spreads.
Tested on TypeScript 5.7.
What triggers it
Recursive conditional types. They are enormously useful and they are the only thing that produces this error in practice.
// path-based access into nested objects
type Path<T> = T extends object
? { [K in keyof T]: K extends string ? K | `${K}.${Path<T[K]>}` : never }[keyof T]
: never;
type Config = {
server: { host: string; port: number; tls: { cert: string; key: string } };
db: { url: string; pool: { min: number; max: number } };
};
type P = Path<Config>; // works for shallow objects, explodes for deep ones
For a three level object this is fine. For a deeply nested config, or worse a recursive type where an object can contain itself, the checker unrolls until it hits the limit.
The same problem appears in DeepPartial, DeepReadonly, JSON type definitions, ORM query builder types, and anything doing string manipulation at the type level.
Why the limit exists
Type checking is computation, and recursive conditional types are Turing complete. TypeScript could genuinely run forever on a type expression, so there is a governor.
The limit is not a bug and raising it is not an option. There is no compiler flag for it. The fix is always to make the type cheaper.
Worth understanding the failure mode: when TypeScript gives up, the type becomes any. So the error is not just noise, it means you have silently lost type safety in that position. Ignoring TS2589 with a suppression comment is worse than it looks.
The fixes
1. Depth limiting
The standard technique. Carry a counter as a tuple and stop when it runs out.
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
type Path<T, D extends number = 5> = [D] extends [never]
? never
: T extends object
? { [K in keyof T]-?: K extends string
? K | `${K}.${Path<T[K], Prev[D]>}`
: never
}[keyof T]
: never;
type P = Path<Config>; // terminates at depth 5
Prev[D] decrements the counter. When it reaches never, the first branch stops the recursion.
Five levels covers essentially every real configuration object. If you genuinely need more, extend the Prev tuple, but the cost grows and at some point the answer is that the type is doing too much.
2. Simplify to what you actually need
Frequently the elaborate type is solving a problem you do not have.
// clever, expensive, fragile
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
// what you probably wanted
type ConfigOverrides = {
server?: Partial<Config["server"]>;
db?: Partial<Config["db"]>;
};
The second is more typing and it is obvious what it does, it never blows up, and the error messages when it fails are readable. That last point is undersold. A failing recursive type produces error output that is genuinely difficult to read, and a hand written type produces something you can act on.
I have come round to the view that type level cleverness has a maintenance cost most codebases should not pay. If the type is harder to understand than the code it describes, it is not helping.
3. Break recursion with a named interface
TypeScript handles recursion through interfaces much better than through inline conditional types, because interfaces are lazily resolved.
// explodes
type Tree = { value: number; children: Tree[] };
type DeepReadonlyTree = DeepReadonly<Tree>;
// fine
interface ReadonlyTree {
readonly value: number;
readonly children: readonly ReadonlyTree[];
}
Writing the recursive case explicitly costs a few lines and removes the problem entirely.
Finding which type is responsible
The error frequently points at a usage site rather than at the type definition, which is unhelpful when you have several complex types in play.
Generate a trace:
tsc --generateTrace ./trace
That produces trace.json and types.json. Open chrome://tracing or Perfetto and load trace.json. You get a flame graph of the checker's work, and the wide bar is your problem type.
For a quicker signal:
tsc --diagnostics
Types: 184203
Instantiations: 28471003
Memory used: 1247382K
Check time: 18.4s
Instantiations in the tens of millions means a type is being expanded far more than it should. On a healthy medium sized project this number is usually in the low millions.
--extendedDiagnostics breaks the time down further, and comparing before and after a change tells you whether your fix actually helped or just moved the error.
This is the same measurement discipline as reading a query plan: the instinct is to guess at the expensive part, and the tooling will tell you in ten seconds.
The related error
error TS2321: Excessive stack depth comparing types 'X' and 'Y'.
Different limit, similar cause. This one fires when comparing two deeply nested types for assignability rather than when instantiating one.
It shows up most with large union types. A union of 500 string literals compared against another large union is quadratic work, and generic constraints over big unions are a common trigger.
The fix is usually to narrow before comparing, or to replace a large literal union with a branded string type if you do not actually need exhaustiveness checking on it.
Keeping compile times sane
TS2589 is the extreme case of a spectrum. Long before you hit the limit, expensive types make your editor slow, which is a worse day to day cost than a build that takes an extra minute.
A few habits:
Prefer interfaces to type aliases for object shapes. Interfaces are cached and lazily resolved, type aliases with conditionals are eagerly expanded.
Avoid deep generic nesting in public API signatures. Every call site re-instantiates the type. A helper used in 400 places with a complex generic return type is 400 instantiations.
Be careful with large template literal types. ${Uppercase<A>}-${B} over two unions of 50 members produces 2500 types. It is easy to write an expression that generates millions.
Watch Instantiations in CI. Add tsc --diagnostics to a build step and fail if it grows past a threshold. This is a slightly unusual thing to monitor and it catches a category of slow degradation that nobody notices until the editor becomes unusable.
Use skipLibCheck: true. Most projects should have this on. Type checking your dependencies' declaration files is usually wasted work, and a single badly written .d.ts in a dependency can cause this error in your build with no way for you to fix it.
The judgement call
There is a real tension here. Precise types catch real bugs, and the machinery that makes them precise is exactly what triggers this error.
My working rule: type level programming is worth it in library code, where one complex type serves hundreds of call sites and the cost is paid once by the author. In application code it usually is not, because the reader is a colleague who now has to understand both the runtime logic and a type system program that describes it.
When you hit TS2589 in application code, the honest first question is whether the type needs to be that clever at all.