Repository navigation
fix(64998): defer index signature value types for keyof circularity - #65039
timurzvural wants to merge 2 commits into
Conversation
Declaration emit of `type Status = keyof StatusClass | ""` (with StatusClass members and a string index signature typed as Status) collapsed to `any` when status.ts and consumer.ts were assigned to different checkers. keyof only needs index key types, but getIndexInfosOfIndexSymbol eagerly resolved value types, and class instantiation (via the implicit `this` type) re-resolved them during Status's own alias resolution, incorrectly marking Status circular. Defer value-type resolution until first use, preserving mappers across instantiation. Fixes microsoft#64998 Co-authored-by: timurzvural <timurzvural@gmail.com>
There was a problem hiding this comment.
🟡 Changes recommended
Recursive index-signature resolution can cache an in-progress sentinel as an any-flagged type without a circularity diagnostic, requiring cycle-tracking integration and negative regression tests.
1 open issue
What changed in this PR
This PR addresses #64998, where declaration emit can silently replace a circular keyof alias with any depending on checker assignment.
Changes:
- Defer index-signature value resolution and preserve deferred values across instantiation.
- Route value-type consumers through a lazy getter, including API serialization.
- Add a declaration-emit regression test covering checker counts 1–4.
| File | Description |
|---|---|
tsc/internal/compiler/declaration_checkers_64998_test.go |
Tests declaration output across checker counts. |
tsc/internal/checker/types.go |
Stores deferred mappers and documents cached value access. |
tsc/internal/checker/relater.go |
Resolves index values during type comparisons. |
tsc/internal/checker/nodebuilderimpl.go |
Resolves deferred values when building index signatures. |
tsc/internal/checker/nodebuilder_hover.go |
Uses lazy values when comparing hover signatures. |
tsc/internal/checker/jsx.go |
Resolves indexed JSX attribute types lazily. |
tsc/internal/checker/inference.go |
Updates inference to use the lazy getter. |
tsc/internal/checker/flow.go |
Resolves index values during destructuring analysis. |
tsc/internal/checker/exports.go |
Exposes the value-resolution getter. |
tsc/internal/checker/checker.go |
Implements deferred resolution and updates value consumers. |
tsc/internal/api/session.go |
Resolves index values before API serialization. |
Review effort: Balanced
| info.valueType = c.unresolvedType | ||
| info.valueType = c.instantiateType(c.getTypeFromTypeNode(returnTypeNode), info.mapper) |
There was a problem hiding this comment.
Addressed in 119d74c: deferred index value types now go through pushTypeResolution/popTypeResolution on the *IndexInfo (new TypeSystemPropertyNameIndexInfoValueType). Re-entry returns errorType without caching the in-progress sentinel, and circularity reports TS2577 on the index signature annotation. Added negative tests in circular_index_64998_test.go for direct I[string] recursion and alias-mediated recursion (index-first and alias-first), plus a control that interface Rec { [k: string]: Rec } still type-checks cleanly.
Address PR review: do not cache the in-progress unresolved sentinel as a
usable any-flagged type for recursive index signatures such as
`interface I { [k: string]: I[string] }`. Resolve deferred value types via
the type-resolution stack and report TS2577 on circularity. Add negative
tests for direct recursion and alias-mediated recursion (index-first and
alias-first), plus a control that recursive `interface Rec { [k: string]: Rec }`
still succeeds.
Co-authored-by: timurzvural <timurzvural@gmail.com>

Bug
With
--checkers≥ 3 (multi-checker pools), declaration emit of this pattern collapsescurrenttoanywith no diagnostic:Expected (and
--singleThreaded/ checkers 1–2):export declare const current: import("./status").Status;Actual with ≥3 checkers:
export declare const current: any;The failure is nondeterministic with file→checker assignment: it shows up when
status.tsandconsumer.tsland on different checkers.Root cause
keyofonly needs index key types, butgetIndexInfosOfIndexSymboleagerly resolved index signature value types. WhenStatusis resolved on a checker that did not already finishStatusClassvia the nested success path, computingkeyof StatusClassre-entersStatusthrough the index value type. Classes always go throughinstantiateIndexInfosbecause of the implicitthistype, which re-resolved deferred value types and incorrectly markedStatuscircular →errorType/any.Fix
getIndexInfosOfIndexSymbol, defer resolving index signature value types (leavevalueTypenil when a return type node exists).Checker.getValueTypeOfIndexInfoto resolve lazily from the declaration (and apply any stored mapper).instantiateIndexInfo, keep deferred value types deferred across instantiation (store combined mapper onIndexInfo), so classthis-instantiation duringkeyofdoes not reintroduce circularity..valueTypereads through the lazy getter; exportGetValueTypeOfIndexInfofor the API.Tests
tsc/internal/compiler/declaration_checkers_64998_test.gocovering checkers 1–4 against the reduced fixture.Related
Fixes #64998
AI assistance disclosure
This PR was authored with assistance from Cursor Agent. I have read and understand the change and am prepared to discuss and revise it in review.