Skip to content

Fix template literal type exponential blowup in addSpans#3175

Open
Copilot wants to merge 1 commit intomainfrom
copilot/fix-memory-consumption-issue
Open

Fix template literal type exponential blowup in addSpans#3175
Copilot wants to merge 1 commit intomainfrom
copilot/fix-memory-consumption-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 20, 2026

Recursive template literal types like `${S}_${S}` can double string length each iteration, exhausting memory before the instantiation depth limit (100) or tail recursion limit (1000) is reached.

Repro from microsoft/TypeScript#63271:

type Dec<N extends number> = 
    N extends 5 ? 4 : N extends 4 ? 3 : N extends 3 ? 2 : 
    N extends 2 ? any : 
    N extends 1 ? 0 : 0;

type Recur<N extends number, S extends string> = 
    N extends 0 ? S : Recur<Dec<N>, `${S}_${S}`>;

type Explode = {
    [P in Recur<5, "a"> as `${P}_key`]: any;
};

Two growth vectors exist in addSpans:

  • Concrete strings double via string builder concatenation of resolved literals
  • Generic placeholders double when flattening nested template literal types containing string placeholders

Added two guards in the addSpans loop in getTemplateLiteralType:

  • sb.Len() > 100_000 — bounds concrete string growth
  • len(newTypes) > 1_000 — bounds placeholder count growth

Either triggers addSpans returning false, which falls back to stringType. The existing tail recursion limit then produces the expected TS2589 error.

Add length checks in getTemplateLiteralType's addSpans closure to prevent
exponentially growing template literal types from consuming all memory.
Two checks are added:
1. sb.Len() > 100_000: caps concrete string literal growth
2. len(newTypes) > 1_000: caps generic placeholder growth

When either limit is exceeded, addSpans returns false, causing
getTemplateLiteralType to safely return stringType instead of
building an infinitely growing type.

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/513b1414-7f99-4616-b0a7-57a74d4b4eb6
@RyanCavanaugh RyanCavanaugh marked this pull request as ready for review March 20, 2026 15:41
Copilot AI review requested due to automatic review settings March 20, 2026 15:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds guardrails to prevent exponential memory blowups when instantiating recursive template literal types, and introduces a regression test reproducing the TypeScript issue.

Changes:

  • Add limits in getTemplateLiteralType/addSpans to cap concrete string growth and placeholder/type expansion.
  • Add a compiler test case reproducing exponential blowup in recursive template literal types.
  • Add baseline .types, .symbols, and .errors.txt outputs for the new test.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
internal/checker/checker.go Adds early-exit guards in addSpans to prevent unbounded growth and fall back to stringType.
testdata/tests/cases/compiler/templateLiteralExponentialBlowup.ts New regression test reproducing recursive template literal exponential growth.
testdata/baselines/reference/compiler/templateLiteralExponentialBlowup.types Baseline for type output of the new regression test.
testdata/baselines/reference/compiler/templateLiteralExponentialBlowup.symbols Baseline for symbol output of the new regression test.
testdata/baselines/reference/compiler/templateLiteralExponentialBlowup.errors.txt Baseline confirming expected TS2589 error for the new regression test.
Comments suppressed due to low confidence (2)

internal/checker/checker.go:1

  • The guard is checked only once per loop iteration, before any sb.WriteString(...) or newTypes appends happen. A single iteration can still append a very large literal chunk or expand newTypes by many entries (e.g., when flattening nested template literal types), potentially blowing past the intended bounds before the next iteration checks the limits. Consider enforcing the limits at the exact growth sites: (1) before/after each sb.WriteString (preferably pre-checking sb.Len()+len(toAdd)), and (2) after each append/extend to newTypes (or via a small helper like appendNewType(...) bool that checks the cap immediately).
    internal/checker/checker.go:1
  • The newly introduced numeric limits are magic numbers. To make the behavior easier to tune and to document the rationale, define named constants (ideally near other instantiation/recursion limits) such as maxTemplateLiteralConcreteLength and maxTemplateLiteralPlaceholderCount, and add a short comment explaining why these specific thresholds were chosen and how they relate to existing depth/tail-recursion limits.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants