TypeScript

Type instantiation is excessively deep: defer or cap it

Make a recursive union type compile inside generic functions again, without losing the payload checks on concrete modules.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

A recursive type that indexes into a mapped type to build one big union, like AllModuleActions<M> in the Stack Overflow question, is fine on a concrete module but explodes when M is still an unresolved generic. The checker keeps creating new instantiations until it hits a hard limit, then gives up with the error. Either defer the evaluation by writing the parameter as M extends any ? AllModuleActions<M> : never, or give the recursion an explicit depth counter so it stops after a few levels. Upgrading TypeScript does not make this code compile.

The message as the compiler prints it, on the question's own code:

text
original.ts(69,3): error TS2589: Type instantiation is excessively deep and possibly infinite.

Why it happens#

The question defines a Module as a pair of payloads and children, where children can be more modules. ModuleRootActions<M> turns the payloads into a union of { type, payload } objects through an indexed access type on a mapped type. AllModuleActions<M> then does the same for every child, calling itself for each one, and joins everything into one union type.

As the accepted answer puts it, this indexes "arbitrarily deep into an object type all at once to produce a big union type". The answer adds that the problem does not usually appear when the module is a specific type, only when it is an unspecified generic type parameter, and that its author does not know exactly why. The reproduction below matches that: the concrete A → B → C tree compiles, and the same type applied to MODULE inside generic does not.

What the checker does reveal is where it gives up. It has two hard limits. In the TypeScript 5.9.3 source, instantiateTypeWithAlias in checker.ts stops when the nesting reaches 100 or when one statement or expression has caused five million instantiations. The comment there reads:

text
// We have reached 100 recursive type instantiations, or 5M type instantiations caused by the same statement
// or expression. There is a very high likelyhood we're dealing with a combination of infinite generic types
// that perpetually generate new type identities, so we stop the recursion here by yielding the error type.

The Go port of the checker in the microsoft/typescript-go repository has the same two limits.

The question's code shows which limit it hits. Compiled with TypeScript 4.5.4 (the release that was current when the question was asked) and the extended diagnostics flag, the faulty file reports this:

text
original.ts(69,3): error TS2589: Type instantiation is excessively deep and possibly infinite.
Instantiations:            5005832

That count is just over five million, consistent with the count limit rather than the depth limit. The two fixed versions below need a few thousand instantiations on the same compiler.

The error points at the use, not at the type#

Line 69 of the question's file is not the type definition. It is the bare action; statement inside generic. Two controls in the reproduction confirm that the type is only a problem when it is evaluated against a generic:

  • const concreteAction: ConcreteAction<A> in the same file compiles. The concrete tree ends.
  • The same file with only the action; line removed compiles with no error on TypeScript 4.5.4 and 7.0.2.

In this file, then, the error is reported at the statement that uses the value, not at the type alias. In your own project, check the line the error points to and the type of the value used there.

Fix#

Both fixes come from the accepted answer. Try the first one before the second, because it keeps the type exactly as precise as before.

  1. Defer the evaluation with a distributive conditional type. Change only the parameter type of the generic function. Rewriting it as a distributive conditional type over the bare type parameter can make the compiler defer evaluation while MODULE is unresolved. The accepted answer offers it as something to try, not a guarantee, but on the question's code it works: on TypeScript 4.5.4 the instantiation count falls from just over five million to 6,126:
ts
const generic = <MODULE extends Module<any, any>>(
  action: MODULE extends any ? AllModuleActions<MODULE> : never,
) => {
  action;
};
  1. If deferring is not enough, cap the recursion depth. Add a counter parameter that starts at 4 and goes down by one at each level through a lookup tuple. When it reaches never, the recursion stops:
ts
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
 
type AllModuleActions<M extends Module<any, any>, D extends Prev[number] = 4> =
  [D] extends [never] ? never :
  | ModuleRootActions<M>
  | {
    [K in keyof M["children"]]: M["children"][K] extends Module<any, any>
    ? AllModuleActions<M["children"][K], Prev[D]>
    : never;
  }[keyof M["children"]];

Prev[4] is 3, Prev[3] is 2, and so on, so even an unresolved M can only be expanded a fixed number of times. The question's generic function then compiles unchanged.

  1. Do not rely on an upgrade. One answer on the question reports that moving from 3.9 to 4.6.3 fixed their project. For this code it does not: TypeScript 4.6.3 reports the same error at the same line, and so does TypeScript 7.0.2, the latest release on npm as of 20 October 2026. An upgrade can help when the problem was a bug in an old release, not when the type itself recurses without a bound.

Verify the fix#

Run the project's own compiler against your tsconfig with --noEmit (on Windows, through an npm script if a bare tsc command is not found, as explained in the tsc is not recognized fix). The reproduction compiled a single file with --strict; in a project, keep your own settings so unrelated errors do not appear. The error should be gone. That alone does not prove the type still works, because a type that had become too loose would compile too. So add one valid call and one wrong call after the function:

ts
generic<A>({ type: "incrementBy", payload: 1 });
generic<A>({ type: "incrementBy", payload: "x" });

The first line, an action from the grandchild module C, must compile. The second must fail. With the deferred version on TypeScript 7.0.2, only the wrong call is rejected:

text
deferred_check.ts(74,12): error TS2345: Argument of type '{ type: "incrementBy"; payload: string; }' is not assignable to parameter of type 'AllModuleActions<A, { b: B; }>'.
  Types of property 'payload' are incompatible.
    Type 'string' is not assignable to type 'number'.

The depth-limited version rejects the same call on 4.5.4 and on 7.0.2. Its message lists the members of the union instead, and the valid call on line 73 passes in both cases.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the depth cap silently drops actions#

The depth counter has a cost the error message will never show: anything below the cap simply drops out of the union. A chain of six modules, from L0 down to L5, each with one payload named after its level, shows it. With the default cap of 4, the root plus four levels are kept and the action from L5 disappears:

text
depth6.ts(80,7): error TS2820: Type '"level5"' is not assignable to type '"level0" | "level1" | "level2" | "level3" | "level4"'. Did you mean '"level0"'?

Passing a larger depth as the second type argument, AllModuleActions<L0, 6>, brings "level5" back, and that line compiles. Pick the cap from the real depth of your module tree, not from the example. The Prev tuple also sets the upper bound: it only counts down from 9, so a deeper tree needs a longer tuple.

What it looked like on TypeScript 5.9.3#

TypeScript 5.9.3 behaved differently on the faulty file. In a container limited to 600 MB of RAM, it never printed the error. It ran out of memory instead, first with Node's default heap and again after about a minute with a 540 MB heap:

text
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

The fixed files compiled without problems on the same compiler and in the same container. If the type check of a project with recursive types like this one crashes with that message rather than printing the error, treat it as the same problem first, before raising the memory cap. The heap limit page explains how to find which process ran out of memory and where its limit is set. Raising the heap may only postpone a recursion that has no bound.

When it only fails inside one project#

The question's case fits in one file. The still-open TypeScript issue #34933 describes the opposite situation: the error appears only in the reporter's codebase, while the same code works in the TypeScript Playground. That report involves React, React Router and recompose types, and the reporter noticed that replacing Omit<Props, never> with Props made the error go away. If your case looks like that one, the fixes above may not apply directly, and the issue is the place to compare notes. The question's Module<any, any> constraints rely on any. If your lint setup objects to that, the Unexpected any fix covers the options. For the indexed access errors you can hit while restructuring these types, see the TS7053 indexing fix.

Everything above was reproduced on 20 October 2026 in a node:22-slim container, with the question's playground code decoded verbatim and compiled by the npm builds of TypeScript 4.5.4, 4.6.3, 5.9.3 and 7.0.2. In this case the recursion inside the type was the cause, not the TypeScript version.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote