Repository navigation
Keep inferring from type arguments when the same generic type is nested inside them - #65077
kennysarnoski-jarvis wants to merge 2 commits into
Conversation
…ed inside them Since microsoft#64553, inference between the type arguments of two references to the same generic type (or alias) runs through invokeOnce, which pushes both types onto the recursion stacks and stops with a circularity when isDeeplyNestedType finds the same recursion identity twice on both sides. For a type nested in its own arguments, such as Eff<() => Eff<A, R>, never>, the outer and inner references share an identity, so the inner pair is treated as a cycle at depth 2 and nothing is inferred for the inner type parameters. R then falls back to unknown. Before microsoft#64553 this path called inferFromTypeArguments directly and had no such guard, which is also what TypeScript 5.9 and 7.0 do. This keeps the cache from microsoft#64553 for the two type-argument paths but routes them through a variant of invokeOnce that leaves the recursion stacks alone and never reports a circularity. Inference from type arguments is bounded by the arguments themselves; deferred references were already excluded, and object types on the way down still go through the guarded invokeOnce. Fixes microsoft#64757. Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The Prisma regression lacks demonstrated coverage because Shape 3 compares the same alias on both sides and was not shown to fail before the fix, unlike the reported repro’s distinct-alias nested-array inference path.
1 open issue
What changed in this PR
Fixes #64757 by allowing inference to continue through nested instances of the same generic type, while preserving the inference cache introduced in #64553.
Changes:
- Separates recursion checking from caching for same-generic type-argument inference.
- Adds compiler cases for nested inference through generic aliases and interfaces.
- Records expected inferred types and symbol bindings.
| File | Description |
|---|---|
tsc/testdata/tests/cases/compiler/inferFromNestedTypeArgumentsOfSameGeneric.ts |
Adds nested generic inference test cases. |
tsc/testdata/baselines/reference/compiler/inferFromNestedTypeArgumentsOfSameGeneric.types |
Records expected inference results. |
tsc/testdata/baselines/reference/compiler/inferFromNestedTypeArgumentsOfSameGeneric.symbols |
Records test symbol bindings. |
tsc/internal/checker/inference.go |
Keeps type-argument caching without applying the nesting guard. |
Review effort: Balanced
| declare function viaAlias<T, S>(p: Payload<Payload<Selected<T, S>>>, s: S): Selected<T, S>; | ||
| declare const p: Payload<Payload<{ id: number }>>; | ||
| const r4 = viaAlias(p, { select: { id: true } }); | ||
| const check4: { id: number } = r4; |
There was a problem hiding this comment.
Note
🤖 Claude Mythos 5.1 responding on behalf of Kenny
Agreed, that reduction did not fail before the fix. Shape 3 is now the repro from the issue (generic alias as a parameter type, inference through nested arrays of a recursively computed type). Before the fix it fails with TS2571 on viaAlias; with it, both inline and viaAlias are string, and the baselines are regenerated (132d27c).
|
@microsoft-github-policy-service agree |
The previous Shape 3 compared the same alias on both sides and did not fail before the fix. This is the reduction from the issue: a generic alias as a parameter type, with inference descending through nested arrays of a recursively computed type. It fails with TS2571 before the fix. Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

Note
🤖 Claude Mythos 5.1 writing on behalf of Kenny
Fixes #64757.
AI disclosure, per CONTRIBUTING.md. Kenny Sarnoski (
kennysarnoski-jarvis, who filed #64757) chose this issue and is shepherding this PR himself. He and Claude (Mythos 5.1, Anthropic) worked out the cause and the fix together in a Claude Code session he directed; Claude wrote the patch, the test and this description. He has read the change and will answer review feedback. This is a single, targeted PR for his own issue, not part of any queue-driven workflow.What breaks
Since
7.1.0-dev.20261001.1, inferring from an argument whose type is a generic type nested inside its own type arguments, such asEff<() => Eff<A, R>, never>, drops the inferences for the inner type parameters when the parameter type comes from an instantiated generic alias or interface.Rfalls back tounknown.7.0.2and7.1.0-dev.20260930.4infer it. Three real-world shapes are in #64757: Effect'sHttpRouter.middleware(...).layer(which becomes an error string), Prisma's nestedselectresults, andSaga.toLayerfrom alchemy.run (137 errors and 285 false Effect diagnostics in one app).Cause
#64553 routed the two "same generic type, infer from the type arguments" paths in
inferFromTypesthroughinvokeOnceso the new cache could cover them.invokeOncealso pushes the pair onto the recursion stacks and stops with a circularity whenisDeeplyNestedType(..., 2)finds the same recursion identity twice on both sides. The outerEff<...>and the innerEff<A, R>share an identity, so the inner pair is reported as a cycle andinferFromTypeArgumentsnever runs for it. Before #64553 those paths calledinferFromTypeArgumentsdirectly, without the guard, as TypeScript 5.9 and 7.0 still do.Fix
invokeOnceis split into a worker with acheckRecursionflag. The two type-argument paths go throughinvokeOnceWithoutRecursionCheck, which keeps the cache (and its circularity marker for true re-entry) but leaves the recursion stacks alone and never reports a circularity. Inference from type arguments is bounded by the arguments themselves; deferred type references were already excluded from this path, and object types reached on the way down still go through the guardedinvokeOnce. The other three callers (conditional types, generic mapped types, object types) are unchanged.Testing
inferFromNestedTypeArgumentsOfSameGeneric.tswith the three shapes from Regression in 7.1 (#64553): R inferred as unknown through a generic type alias call signature with a nested same-type argument #64757, the Prisma one as reported. Onmainit fails with four errors: threeTS2322([unknown]/unknownwhere[Request],neverandRequestare expected) and oneTS2571on the PrismaviaAliasline; with this change the.typesbaseline shows the expected types and there is no.errors.txt.go test ./...intscpasses (afternpm ci, whichinternal/astnavneeds).correlatedUnions.tsfrom Cache inferences made from type arguments #64553 keeps its current baseline.HttpRouter.middleware(...).layertypes as string (requires inferredunknown); 0.3.0 and tsc 7.1.0-dev accept pingdotgg/ts-rust#79 and an Effect-based server (74 errors onmain, 0 with this change, with no source changes) both typecheck.🤖 Generated with Claude Code