fix(checker): cap template literal type size to avoid unbounded growth - #64194
Open
Eugene Kalinin (ekalinin) wants to merge 2 commits into
Open
fix(checker): cap template literal type size to avoid unbounded growth#64194Eugene Kalinin (ekalinin) wants to merge 2 commits into
Eugene Kalinin (ekalinin) wants to merge 2 commits into
Conversation
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 started reviewing on behalf of
Eugene Kalinin (ekalinin)
September 7, 2026 20:24
View session
Contributor
There was a problem hiding this comment.
🟡 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.
…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 started reviewing on behalf of
Eugene Kalinin (ekalinin)
September 7, 2026 20:44
View session
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.
Fixes #63271
The repro from the issue is a recursive conditional type whose check type becomes
anyafter a few iterations:With
N = any,getConditionalTypeincludes both branches and keeps tail-recursing withDec<any> = any, whileSdoubles 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
getTemplateLiteralTypenow bounds the size of the type it produces and reports TS2589 (Type instantiation is excessively deep and possibly infinite) when a limit is exceeded, returningerrorType. This mirrors howcheckCrossProductUnionbounds union sizes.Two limits are needed:
errorType,anyis a valid placeholder, so${any}_${any}starts doubling the number of spans instead of the text. The same growth happens in tsc.js forRecur<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.tscovers both growth patterns. Each reports exactly one TS2589.hereby testandhereby lintpass. No existing baselines changed.