Skip to content

Bound template literal expansion while computing base constraints - #65428

Open
Rao Tao (Keviniscool-boy) wants to merge 2 commits into
microsoft:mainfrom
Keviniscool-boy:fix/65095-template-expansion-budget
Open

Rao Tao (Keviniscool-boy) wants to merge 2 commits into
microsoft:mainfrom
Keviniscool-boy:fix/65095-template-expansion-budget

Conversation

@Keviniscool-boy

@Keviniscool-boy Rao Tao (Keviniscool-boy) commented Nov 10, 2026 •

Copy link
Copy Markdown

Summary

Fixes #65095

Contextually typing an object literal against a recursive conditional type built from a large catalog (next-intl's NamespaceKeys / NamespacedMessageKeys) created tens of millions of string literal types and grew memory without bound: 7.3M live literal types at 1,000 messages, OOM past 7 GB at 2,000 messages, where typescript@6.0.3 stays at 2.5 GB.

Root cause (from heap/CPU profiles of a self-built compiler on the issue's repro): computing the base constraint of the contextual type resolves the recursive conditional for every member of the contextual union (computeBaseConstraint → getConstraintFromConditionalType → getConstraintOfDistributiveConditionalType), and each resolution distributes template literal cross products (getTemplateLiteralType → getStringLiteralType). Every created literal/template type is retained for the checker's lifetime in the stringLiteralTypes / templateLiteralTypes caches. checkCrossProductUnion bounds each individual distribution to <100k combinations, but the products compound across the many distinct conditional instantiations, so nothing sees the total.

Fix: count every template literal expansion performed while computing a single base constraint against a shared budget (maxTemplateExpansionPerBaseConstraint = 1M, matching the scale of the existing union-complexity limit). Once exhausted, getTemplateLiteralType falls back to the string type — a sound base constraint for any template literal type, and the same fallback the function already uses when a span's constraint is unrepresentable. The budget only engages while a base constraint is being computed (outermost getResolvedBaseConstraint), so normal template literal construction is untouched.

Before/after on the issue's reproduction (Linux x64, peak RSS)

Messages typescript 6.0.3 main this PR
1,000 1,914 MB 2,650 MB 216 MB
2,000 2,500 MB >7 GB (OOM) 240 MB

The same four diagnostics from the issue (TS2345, TS2590, TS2345, TS2589) are still reported in the same positions; only the elaborations of the pathological unions shorten, since the fallback replaces full expansion past the budget.

Test plan

  • The issue's reproduction script (gen.py at 1,000 and 2,000 messages) before/after with /usr/bin/time -f %M: numbers above; the 2,000-message run on main is killed at the memory cap and completes in 4.5 s with this change.
  • go test ./... in tsc/: all packages pass (includes the full baseline suite).

AI note

This change was prepared with AI assistance.

Contextually typing an object literal against a recursive conditional
type built from a large catalog (next-intl's NamespaceKeys and
NamespacedMessageKeys) could create tens of millions of string literal
types: the base constraint computation resolves the conditional for
every member of the contextual union, and each resolution distributes
template literal cross products whose results are retained in the
stringLiteralTypes and templateLiteralTypes caches for the lifetime of
the checker. The per-call cross product check in checkCrossProductUnion
bounds each distribution, but the products compound across the many
distinct conditional instantiations, so memory grew quadratically with
the catalog and peaked past 7 GB at 2,000 messages where TypeScript
6.0.3 stays at 2.5 GB.

Count every template literal expansion performed while computing a
single base constraint against a shared budget of one million
expansions, and fall back to the string type once exhausted. The string
type is a sound base constraint for any template literal type, and the
fallback only engages in the pathological regime that previously
exhausted memory.

On the issue's reproduction this reduces peak RSS from 2,650 MB to
216 MB at 1,000 messages, and from an OOM kill past 7 GB to 240 MB at
2,000 messages, while producing the same four diagnostics.

Fixes microsoft#65095
Copilot AI balanced review requested due to automatic review settings November 10, 2026 15:30
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Nov 10, 2026
@typescript-automation typescript-automation Bot added the For Backlog Bug PRs that fix a backlog bug label Nov 10, 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@Keviniscool-boy

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@jakebailey Jake Bailey (jakebailey) added the Domain: Performance Performance related label Nov 11, 2026

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

Domain: Performance Performance related 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)

3 participants