Repository navigation
Stop unbounded memory growth after type complexity limits are hit - #65308
Harsh Vardhan Chauhan (1-DARK) wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 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
| type TooBig = `${A}.${B}.${C}`; | ||
| declare function f<T extends string>(x: T): T; | ||
| f<TooBig>("a0.b0.c0"); | ||
| type Nested = `${TooBig}.${A}`; |
a53f871 to
95b60dd
Compare
There was a problem hiding this comment.
🟡 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
| 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 |
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.
95b60dd to
4595875
Compare
There was a problem hiding this comment.
🟡 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 { |


Fixes #65095
Problem
With a large
next-intlmessage 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:
instantiateTypeWithAliasallocated 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.getTemplateLiteralTypekept 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)complexityLimitNode, set when TS2589 (count limit) or TS2590 (cross product) fires;getTemplateLiteralTypereturnserrorTypefor further union expansion on that node.getTemplateLiteralType, stop expanding remaining constituents as soon as one yieldserrorType.templateLiteralComplexityLimit(TS2590 cascade through a call and a nested template) with baselines.Results
Repro from the issue (macOS, same four diagnostics before and after):
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
errorType, so follow-on errors there may be suppressed. Existing baselines are unchanged.getIntersectionType; this PR addresses memory, not that.go test ./internal/checker/... ./internal/testrunner/...passes.