Repository navigation
Bound template literal expansion while computing base constraints - #65428
Open
Rao Tao (Keviniscool-boy) wants to merge 2 commits into
Open
Rao Tao (Keviniscool-boy) wants to merge 2 commits into
Rao Tao (Keviniscool-boy) wants to merge 2 commits into
Conversation
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
Author
|
@microsoft-github-policy-service agree |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, wheretypescript@6.0.3stays 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 thestringLiteralTypes/templateLiteralTypescaches.checkCrossProductUnionbounds 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,getTemplateLiteralTypefalls back to thestringtype — 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 (outermostgetResolvedBaseConstraint), so normal template literal construction is untouched.Before/after on the issue's reproduction (Linux x64, peak RSS)
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
gen.pyat 1,000 and 2,000 messages) before/after with/usr/bin/time -f %M: numbers above; the 2,000-message run onmainis killed at the memory cap and completes in 4.5 s with this change.go test ./...intsc/: all packages pass (includes the full baseline suite).AI note
This change was prepared with AI assistance.