Skip to content

checker: read a type's declared members while its base types resolve - #21

Closed
RedesignedRobot wants to merge 1 commit into
pingdotgg:mainfrom
RedesignedRobot:checker-zod-json-ts5115
Closed

RedesignedRobot wants to merge 1 commit into
pingdotgg:mainfrom
RedesignedRobot:checker-zod-json-ts5115

Conversation

@RedesignedRobot

@RedesignedRobot RedesignedRobot commented Oct 7, 2026 •

Copy link
Copy Markdown

Fixes #20.

With Zod 4.5.0 to 4.6.5, any z.infer of an object holding z.json() reports TS5115 ($ZodTypeInternals, $InferZodRecordInput, IsOptionalIn). tsc 7.0.2 checks it clean. The pinned Go has the same error, from microsoft/TypeScript#64372.

Cause

microsoft/TypeScript#64372 stopped resolveObjectTypeMembers from storing declared members before the base types resolve, because the partial list caused ghost errors (microsoft/TypeScript#62180). Now a reference back to the type from a base type argument starts member resolution over, which instantiates the same bases again until the depth limit.

Zod hits that loop. $ZodRecordInternals<ZodString, ZodJSONSchema> has $InferZodRecordInput in its base type arguments, and that reads optin through IsOptionalIn over the union members, which leads back to $ZodRecordInternals<ZodString, ZodJSONSchema> itself. But that type declares optin, and addInheritedMembers never replaces a declared value member, so the answer is known before the bases resolve. The cycle isn't real. The reduced example in microsoft/TypeScript#64605 reaches the same point through isStringIndexSignatureOnlyType.

Change

While resolve_object_type_members adds inherited members, it keeps the type's declared members on a small stack. During that window:

  • get_property_of_type_ex returns a value member the type declares itself, which is the same symbol the resolved members will hold.
  • is_string_index_signature_only_type_worker returns false for a type that declares a property.

Every other query resolves the members again, as microsoft/TypeScript#64372 does, and nothing partial is stored on the type. Two unit tests in checker_p21.rs cover the Zod reduction plus the #64605 example, and the #64372 noGhostErrors and keyofGenericExtendingClassDoubleLayer cases. The first test fails with TS5115 when the change is off.

Testing

Case 0.1.0 This branch tsc 7.0.2
#20 repro, zod 4.6.5 TS5115 clean clean
z.json() in arrays, records, unions, tuples, maps, .pipe(), with and without skipLibCheck TS5115 clean clean
microsoft/TypeScript#64605 example TS5115 clean clean
#64372 noGhostErrors clean clean ghost TS2741
keyofGenericExtendingClassDoubleLayer TS5115 TS5115 clean
31-config monorepo on Zod 4.6 88 lines 18 18
7-config monorepo on Zod 4.6 92 lines 89 89

On all three of my projects this branch matches tsc 7.0.2 line for line, and every removed line is this TS5115.

Parity with Go

This departs from the pinned Go, in resolveObjectTypeMembers, getPropertyOfTypeEx and isStringIndexSignatureOnlyTypeWorker. One TestLocal baseline moves: mutuallyRecursiveInference.errors.txt loses the TS5114 that #64372 added and returns to its earlier baseline. That error was a false cycle, because X[X['a']] only reads members X declares.

I applied the same 54-line change to the Go checker at the pin. TestLocal, the fourslash tests (including TestNoGhostErrors) and the checker tests all pass there, apart from that one baseline. I'm happy to send the Go version to microsoft/TypeScript#64605 too, so the pin picks it up.

Note

Fix checker to read declared members while base types resolve

  • Adds a ResolvingMembers stack to checker.Checker. resolve_object_type_members pushes the current type with its declared and working member tables before resolving base types, and pops it after inherited members are processed.
  • get_property_of_type now checks this stack first: if the actively resolving type declares a value property with that name, it returns it instead of re-entering member resolution. This fixes ghost errors from circular base-type references (e.g. class C<T> extends C<C["p"]> in checker_p21.rs).
  • is_string_index_signature_only_type_worker in checker_p30.rs returns false early for an actively resolving type that declares a non-reserved value property.
  • Adds regression tests for circular base-type arguments, inherited-member ghost errors, and a Zod-shaped assignment case.
  • Risk: behavior only changes for unresolved object types found in the resolving_members stack in Checker::get_property_of_type; other lookup paths are unchanged.

Macroscope summarized 66f8110.

Summary by CodeRabbit

  • Bug Fixes
    • Improved class and interface type checking when base types refer to properties declared by the derived type.
    • Prevented unresolved inherited members from being treated as available before base types are resolved.
    • Corrected string-index-signature checks for types with unresolved declared properties.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6fcb83a0-04ef-4b7a-bc6d-1a17e5279e76
📥 Commits

Reviewing files that changed from the base of the PR and between 6fa5178 and 66f8110.

📒 Files selected for processing (3)
  • crates/ts_goport/src/checker/checker_p01.rs
  • crates/ts_goport/src/checker/checker_p21.rs
  • crates/ts_goport/src/checker/checker_p30.rs
 ______________________________________________________________________
< Stop swallowing exceptions. They don't become less real in the dark. >
 ----------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@t3dotgg

t3dotgg commented Oct 7, 2026

Copy link
Copy Markdown
Member

Thank you for this, @RedesignedRobot. The analysis of the cycle in resolveObjectTypeMembers is clear and useful.

Our rule is that tsc-rs output stays byte-equal to tsc at the pinned revision (microsoft/TypeScript 673a5f17d713), and we do not patch upstream bugs ahead of the pin. #20 reproduces on the pinned tsc, so a fix here would make tsc-rs differ from it, and maybe from the fix that upstream picks for microsoft/TypeScript#64605. So we will not merge this PR.

The best place for this change is upstream, on microsoft/TypeScript#64605: your reduction and the two tests would help there. When upstream merges a fix, tsc-rs gets it with the next pin bump, and #20 tracks that. Thanks again.

@t3dotgg t3dotgg closed this Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TS5115 on Zod 4.5 to 4.6.5 z.json() types (inherited from upstream, still open there)

2 participants