Skip to content

Skip combined-constraint check while measuring variances to avoid unbounded chase - #64528

Open
Alexis Delrieu (Amatewasu) wants to merge 1 commit into
microsoft:mainfrom
Amatewasu:fix-64423-effective-constraint-chase
Open

Alexis Delrieu (Amatewasu) wants to merge 1 commit into
microsoft:mainfrom
Amatewasu:fix-64423-effective-constraint-chase

Conversation

@Amatewasu

@Amatewasu Alexis Delrieu (Amatewasu) commented Sep 29, 2026 •

Copy link
Copy Markdown

Fixes #64423

Problem

TypeScript 7 runs out of memory (unbounded growth, zero diagnostics) on programs that TypeScript 6 checks fine. Root cause: the combined-constraint comparison in structuredTypeRelatedTo hoists the constraints of the instantiable constituents of an intersection source into a combined constraint and compares it against the target. While measuring variances, the hoisted constraint may itself be an intersection with instantiable constituents (e.g. deferred conditional types produced by heavily overloaded generic builder types), so a failed comparison re-enters the same logic with a structurally new and ever larger source that neither the relation cache nor the deeply-nested-type guards recognize as recursive — the chase grows without bound until the process dies.

Fix

Skip the combined-constraint check while the variance stack is non-empty; it never contributes to the measured relation between the marker types.

Verification (both repros from the issue)

  • JSX repro (@react-three/fiber IntrinsicElements augmentation + <SvgIcon />, exact versions from the issue): before — killed at a 3 GiB / 6 GiB address-space cap with zero output; after — ~4.4 s, ~700 MB RSS, exit 0.
  • three/tsl repro (uv().div(vec2(1, 1)), three@0.185.1 + @types/three@0.185.1): before — killed at the cap with zero output; after — ~3.1 s, ~280 MB RSS, exit 0.
  • Full go test ./... passes (only pre-existing environmental failure in internal/astnav, which requires a locally built JS typescript package in root node_modules; fails identically on unpatched main).

Note on the approach

An earlier variant of this change additionally capped the chase depth (a counter on Relater limiting nested combined-constraint comparisons to 3). That cap regressed tests/cases/compiler/indexedAccessAndNullableNarrowing.ts (new false TS2345 errors on the syncStoreProp pattern from #57693, which both TS 6 and unpatched TS 7 accept), and the depth needed by legitimate code is not bounded by any small constant, so the cap was dropped. The variance-stack guard alone fixes both repros.

Disclosure

This change was generated by an LLM (as was the investigation in the issue thread).

Checklist notes

  • Could not run npx hereby lint / check:format (no root node_modules in this checkout); ran gofmt + go vet (clean) and the full Go test suite instead.
  • No standalone regression test added: both known triggers require the external dependency graphs (three/tsl, R3F/MUI) whose overloaded builder types produce the degenerate hoisted constraints; a self-contained test case could not be extracted.
  • There is an associated issue (TypeScript 7.0.2 silently runs out of memory while typechecking a project that TypeScript 6.0.3 checks successfully #64423)
  • Code is up-to-date with the main branch
  • go test ./... passes
  • npx hereby lint
  • npx hereby check:format
  • New or updated tests validating the change

The combined-constraint comparison in `structuredTypeRelatedTo` hoists the
constraints of the instantiable constituents of an intersection source into
a combined constraint and compares that constraint against the target. While
measuring variances, the hoisted constraint may itself be an intersection
with instantiable constituents (e.g. deferred conditional types, as produced
by heavily overloaded generic builder types), so a failed comparison re-enters
the same logic with a structurally new and ever larger source that neither the
relation cache nor the deeply-nested-type guards recognize as recursive, and
the chase grows without bound until the process runs out of memory.

Skip the check while the variance stack is non-empty; it never contributes to
the measured relation between the marker types.

Fixes microsoft#64423
Copilot AI balanced review requested due to automatic review settings September 29, 2026 13:50
@typescript-automation typescript-automation Bot added For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 29, 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 review overview

🟡 Changes recommended

The compiler fix lacks a checked-in regression test for the unbounded-chase scenario.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Prevents unbounded type-relation expansion during variance measurement.

Changes:

  • Skips combined-constraint checks while the variance stack is active.
  • Documents the recursive growth scenario.
File Description
tsc/​internal/​checker/​relater.go Guards combined-constraint evaluation during variance measurement.

// that the relation cache and the deeply-nested-type guards never recognize as
// recursive, and the chase grows without bound (see microsoft/TypeScript#64423).
// The check never contributes to the measured relation between the marker types.
if result == TernaryFalse && len(r.c.varianceStack) == 0 && (source.flags&TypeFlagsIntersection != 0 || source.flags&TypeFlagsTypeParameter != 0 && target.flags&TypeFlagsUnion != 0) {
@Amatewasu

Copy link
Copy Markdown
Author

Verified against the private project from the issue

I ran the full typecheck of the actual project behind #64423 (a private React 19 + R3F + TSX app, ~855 files) with binaries built from this PR branch vs. unpatched main, using the same methodology as the issue (tsc --noEmit --pretty false --incremental false --composite false under a systemd-run --user -p MemoryMax=… cgroup cap):

Binary Memory cap Result
TS 7 unpatched main 6 GiB OOM-killed after ~3 min, zero output
TS 7 with this PR 6 GiB 25.0 s, ~4.9 GiB peak RSS, exit 0, 0 errors
TS 6.0.3 (8 GB node heap) 12 GiB 146.9 s, ~3.2 GiB peak, exit 0, 0 errors

Diagnostics parity holds: the patched TS 7 reports exactly the same result as TS 6 (zero errors) on the full project, so the guard is not observable in output — it only stops the runaway.

Caveats, for completeness:

  • Peak RSS with the fix (~4.9 GiB) is still higher than TS 6 (~3.2 GiB) on this project; the check is now bounded and completes, but it needs memory headroom.
  • TS 6 with the default 4 GB node heap aborts on this project (exit 134) — consistent with what was reported in the issue thread.

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

For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

TypeScript 7.0.2 silently runs out of memory while typechecking a project that TypeScript 6.0.3 checks successfully

3 participants