Skip to content

fix(checker): cap template literal type size to avoid unbounded growth - #64194

Open
Eugene Kalinin (ekalinin) wants to merge 2 commits into
microsoft:mainfrom
ekalinin:fix/template-literal-type-length-guard
Open

fix(checker): cap template literal type size to avoid unbounded growth#64194
Eugene Kalinin (ekalinin) wants to merge 2 commits into
microsoft:mainfrom
ekalinin:fix/template-literal-type-length-guard

Conversation

@ekalinin

Copy link
Copy Markdown
Contributor

Fixes #63271

The repro from the issue is a recursive conditional type whose check type becomes any after a few iterations:

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;
};

With N = any, getConditionalType includes both branches and keeps tail-recursing with Dec<any> = any, while S doubles on every iteration. The tail recursion limit of 1000 iterations is never reached, because the string grows exponentially and memory runs out first.

The JS compiler threw RangeError: Invalid string length. The Go port has no string length limit, so it never terminates: after 45 seconds the process was at 9.8 GB RSS and still growing.

Fix

getTemplateLiteralType now bounds the size of the type it produces and reports TS2589 (Type instantiation is excessively deep and possibly infinite) when a limit is exceeded, returning errorType. This mirrors how checkCrossProductUnion bounds union sizes.

Two limits are needed:

  • Text length (50,000,000 characters). This is the case from the issue.
  • Number of placeholders (100,000). Once the length guard returns errorType, any is a valid placeholder, so ${any}_${any} starts doubling the number of spans instead of the text. The same growth happens in tsc.js for Recur<5, string>.

After the guard fires, the conditional loop continues cheaply on cached types until the existing tail recursion limit produces the final TS2589. Duplicate diagnostics at the same location are deduplicated, so the user sees a single error.

Choice of limits

The type-challenges test suite for "Length of String 3" (#31824) builds string literal types of up to ~10^7 characters and compiles today, so the length limit keeps a 5x margin over it. Going higher makes the regression test proportionally more expensive: at 100,000,000 it took 55 s and 2.2 GB under -race.

Test

templateLiteralTypeExcessiveLength.ts covers both growth patterns. Each reports exactly one TS2589.

Case Wall time Peak RSS
String doubling (issue repro) ~1 s 430 MB
Placeholder doubling ~0.7 s 182 MB

hereby test and hereby lint pass. No existing baselines changed.

Recursive conditional types with an `any` check type keep tail-recursing
while their template literal argument doubles on every iteration. The
tail recursion limit in getConditionalType is never reached because the
string (or the number of placeholders) grows exponentially and exhausts
memory first.

Bound both the text length and the number of placeholders produced by
getTemplateLiteralType and report TS2589 when a limit is exceeded, in
the same way checkCrossProductUnion bounds union sizes.

Fixes microsoft#63271
Copilot AI balanced review requested due to automatic review settings September 7, 2026 20:23
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Sep 7, 2026
@typescript-automation typescript-automation Bot added the For Backlog Bug PRs that fix a backlog bug label Sep 7, 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 text-length guard can be bypassed by template literals split across multiple segments, leaving memory growth unbounded.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds safeguards against unbounded template-literal type growth in the Go checker.

Changes:

  • Caps template text length and placeholder count.
  • Reports TS2589 when limits are exceeded.
  • Adds regression tests and compiler baselines.
File summaries
File Review
tsc/testdata/tests/cases/compiler/templateLiteralTypeExcessiveLength.ts Adds regression cases.
tsc/testdata/baselines/reference/compiler/templateLiteralTypeExcessiveLength.types Records type baseline.
tsc/testdata/baselines/reference/compiler/templateLiteralTypeExcessiveLength.symbols Records symbol baseline.
tsc/testdata/baselines/reference/compiler/templateLiteralTypeExcessiveLength.js Records emitted output.
tsc/testdata/baselines/reference/compiler/templateLiteralTypeExcessiveLength.errors.txt Records expected diagnostics.
tsc/internal/checker/checker.go Adds size guards. Critical (2 votes): The length check only measures the current builder segment, allowing aggregate text across reset segments to exceed the limit. Track cumulative segment length.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread tsc/internal/checker/checker.go Outdated
…imit

The length guard only measured the current builder segment. Once a
generic placeholder moved the completed segment into newTexts and reset
the builder, the combined text of a type such as `${S}${string}${S}`
was bounded only by the placeholder limit, so the total could still
grow to gigabytes before any error was reported.

Track the combined length of the segments already moved into newTexts
and compare the running total against the limit.

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.

🟢 Approval recommended

All reviewed changes are covered by regression tests, with no unresolved issues.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

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.

Crash: RangeError: Invalid string length in addSpans during instantiation of recursive template literal types

2 participants