Skip to content

fix(64998): defer index signature value types for keyof circularity - #65039

Open
timurzvural wants to merge 2 commits into
microsoft:mainfrom
timurzvural:fix/64998-defer-index-value-types
Open

timurzvural wants to merge 2 commits into
microsoft:mainfrom
timurzvural:fix/64998-defer-index-value-types

Conversation

@timurzvural

Copy link
Copy Markdown

Bug

With --checkers ≥ 3 (multi-checker pools), declaration emit of this pattern collapses current to any with no diagnostic:

// status.ts
import type { Key } from "./types";
export class StatusClass {
  RUNNING: Status = "RUNNING";
  [key: string]: Status;
}
export type Status = keyof StatusClass | "";
export const statusFromAPI: { [key: Key]: Status } = {};

// consumer.ts
import { statusFromAPI } from "./status";
export const current = statusFromAPI["RUNNING"];

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.ts and consumer.ts land on different checkers.

Root cause

keyof only needs index key types, but getIndexInfosOfIndexSymbol eagerly resolved index signature value types. When Status is resolved on a checker that did not already finish StatusClass via the nested success path, computing keyof StatusClass re-enters Status through the index value type. Classes always go through instantiateIndexInfos because of the implicit this type, which re-resolved deferred value types and incorrectly marked Status circular → errorType/any.

Fix

  1. In getIndexInfosOfIndexSymbol, defer resolving index signature value types (leave valueType nil when a return type node exists).
  2. Add Checker.getValueTypeOfIndexInfo to resolve lazily from the declaration (and apply any stored mapper).
  3. In instantiateIndexInfo, keep deferred value types deferred across instantiation (store combined mapper on IndexInfo), so class this-instantiation during keyof does not reintroduce circularity.
  4. Route existing .valueType reads through the lazy getter; export GetValueTypeOfIndexInfo for the API.

Tests

  • New tsc/internal/compiler/declaration_checkers_64998_test.go covering 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.

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>
Copilot AI balanced review requested due to automatic review settings October 29, 2026 21:39
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Oct 29, 2026
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Oct 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.

🟡 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

Comment thread tsc/internal/checker/checker.go Outdated
Comment on lines +26045 to +26046
info.valueType = c.unresolvedType
info.valueType = c.instantiateType(c.getTypeFromTypeNode(returnTypeNode), info.mapper)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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>

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.

declaration emit depends on checker assignment; a type alias becomes any with no error

3 participants