Skip to content

Stop unbounded memory growth after type complexity limits are hit - #65308

Open
Harsh Vardhan Chauhan (1-DARK) wants to merge 2 commits into
microsoft:mainfrom
1-DARK:fix-template-literal-complexity-memory
Open

Harsh Vardhan Chauhan (1-DARK) wants to merge 2 commits into
microsoft:mainfrom
1-DARK:fix-template-literal-complexity-memory

Conversation

@1-DARK

@1-DARK Harsh Vardhan Chauhan (1-DARK) commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #65095

Problem

With a large next-intl message catalog, a call using a missing namespace plus ICU arguments reports the same four errors as 6.0 (TS2345 ×2, TS2590, TS2589), but peak memory grows without bound (6.1 GB at 1,000 messages, OOM at 2,000, ~40 GB on a 14,000-message app).

Root cause

After a limit error was reported, the checker kept doing the work the limit was meant to prevent:

  1. instantiateTypeWithAlias allocated a new TS2589 diagnostic for every further instantiation once the 5M count was exceeded (~40% of allocations). The diagnostics collection dedupes them, but only after allocation.
  2. getTemplateLiteralType kept expanding cross products of large unions into string-literal unions during base constraint computation (computeBaseConstraint), interning millions of literal types that can never be used (8.4M expanded elements at 1,000 messages).

Changes (tsc/internal/checker/checker.go)

  • Report the instantiation-limit error once per node.
  • Add complexityLimitNode, set when TS2589 (count limit) or TS2590 (cross product) fires; getTemplateLiteralType returns errorType for further union expansion on that node.
  • In the union branch of getTemplateLiteralType, stop expanding remaining constituents as soon as one yields errorType.
  • Add regression test templateLiteralComplexityLimit (TS2590 cascade through a call and a nested template) with baselines.

Results

Repro from the issue (macOS, same four diagnostics before and after):

Messages Before After
1,000 2.9 GB, 12 s 178 MB, 1.6 s
2,000 6.6 GB, 77 s 197 MB, 3.5 s
14,000 ~40 GB (per issue) 459 MB, 205 s

Other variants from the issue (reused key names at 14,000 messages, 1 and 4 calls) drop from ~2.7 GB to ~0.2 GB with identical error counts; the no-ICU and existing-namespace controls are unchanged.

Notes

  • Behavior change: after a complexity limit is reported for a node, later template-literal union expansions on that node yield errorType, so follow-on errors there may be suppressed. Existing baselines are unchanged.
  • Check time for the 14,000-message unique-key catalog is still long (205 s), dominated by the legitimate key-union intersection in getIntersectionType; this PR addresses memory, not that.
  • go test ./internal/checker/... ./internal/testrunner/... passes.

Copilot AI balanced review requested due to automatic review settings November 4, 2026 08:37
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Nov 4, 2026
@typescript-automation typescript-automation Bot added the For Backlog Bug PRs that fix a backlog bug label Nov 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The bailout can permanently cache a context-dependent errorType for an unrelated valid alias, suppressing type errors outside the failing node.

2 open issues
What changed in this PR

This PR addresses unbounded memory growth in the native TypeScript checker after type-complexity limits are reached, targeting the large message-catalog scenario in #65095.

Changes:

  • Deduplicates instantiation-count-limit diagnostics per node.
  • Stops further template-literal union expansion after a node hits a complexity limit or a constituent returns errorType.
  • Adds a compiler fixture with diagnostic, type, and symbol baselines.
File Description
tsc/​testdata/​tests/​cases/​compiler/​templateLiteralComplexityLimit.ts Adds a cross-product-limit fixture with subsequent call and nested-template uses.
tsc/​testdata/​baselines/​reference/​compiler/​templateLiteralComplexityLimit.types Records inferred types for the fixture.
tsc/​testdata/​baselines/​reference/​compiler/​templateLiteralComplexityLimit.symbols Records symbol resolution for the fixture.
tsc/​testdata/​baselines/​reference/​compiler/​templateLiteralComplexityLimit.errors.txt Records the expected TS2590 diagnostic.
tsc/​internal/​checker/​checker.go Adds limit tracking, diagnostic deduplication, and template-expansion short-circuits.

Review effort: Balanced

Comment thread tsc/internal/checker/checker.go Resolved
Comment on lines +8 to +11
type TooBig = `${A}.${B}.${C}`;
declare function f<T extends string>(x: T): T;
f<TooBig>("a0.b0.c0");
type Nested = `${TooBig}.${A}`;

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new bailout can silently replace valid template-literal types with cached error types and suppress assignment diagnostics, while the regression tests do not exercise the changed limit-handling paths.

4 open issues

Review effort: Balanced

Comment thread tsc/internal/checker/checker.go Outdated
Comment on lines +29968 to +29972
if c.instantiationCount >= maxInstantiationCount {
// The instantiation count limit was already exceeded for the current statement or expression, so
// instantiateType yields the error type for everything from here on. Expanding further cross products
// would only allocate huge unions of string literals that can never be used.
return c.errorType
Comment thread tsc/internal/checker/checker.go Resolved
Once a statement exceeded the 5M instantiation limit (TS2589), the checker
kept doing the work the limit is meant to prevent:

- instantiateType allocated a new TS2589 diagnostic for every further
  instantiation (millions), only for the diagnostics collection to discard
  them as duplicates.
- Base constraint computation for generic template literal types kept
  expanding cross products of large unions into string literal unions,
  interning millions of literal types that can never be used.

Report the instantiation limit error once per node. Once it has been reported
for the current node, skip expanding the base constraint of generic template
literal types (returning the error type, as instantiateType already does).
This is limited to base constraint computation, which only runs on generic
types, so the error type is not cached for independent non-generic aliases.
Also stop expanding remaining union constituents in getTemplateLiteralType as
soon as one yields the error type.

On the repro from microsoft#65095 (next-intl, missing namespace + ICU args), peak RSS
at 1,000 messages drops from ~2.9 GB to ~180 MB, and at 14,000 messages it is
~460 MB. The reported diagnostics are unchanged.

Fixes microsoft#65095
Covers the TS2590 cross product limit used through a call and a nested
template, and checks that a limit error for one alias does not leak into an
unrelated alias resolved in the same context.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The bailout caches a node-local error type in shared constraints, allowing later invalid assignments to pass, while the added regression does not exercise the affected instantiation-limit path.

4 open issues
2 resolved since last review

Review effort: Balanced

// into a cross product of string literals would only allocate huge unions that can never be used.
// This is scoped to base constraint computation, which is only ever performed for generic types, so
// the error type is not cached for independently declared (non-generic) aliases.
return c.errorType
}
return c.stringNumberSymbolType
case t.flags&TypeFlagsTemplateLiteral != 0:
if c.instantiationLimitReportedNode != nil && c.instantiationLimitReportedNode == c.currentNode && c.instantiationCount >= maxInstantiationCount {

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Backlog Bug PRs that fix a backlog bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

TypeScript 7 memory grows without bound on a next-intl call with a missing namespace and ICU arguments (6.0 stays at 2.5 GB)

2 participants