Skip to content

Keep inferring from type arguments when the same generic type is nested inside them - #65077

Open
kennysarnoski-jarvis wants to merge 2 commits into
microsoft:mainfrom
kennysarnoski-jarvis:fix/64757-nested-type-argument-inference
Open

kennysarnoski-jarvis wants to merge 2 commits into
microsoft:mainfrom
kennysarnoski-jarvis:fix/64757-nested-type-argument-inference

Conversation

@kennysarnoski-jarvis

@kennysarnoski-jarvis kennysarnoski-jarvis commented Oct 31, 2026 •

Copy link
Copy Markdown

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 as Eff<() => Eff<A, R>, never>, drops the inferences for the inner type parameters when the parameter type comes from an instantiated generic alias or interface. R falls back to unknown. 7.0.2 and 7.1.0-dev.20260930.4 infer it. Three real-world shapes are in #64757: Effect's HttpRouter.middleware(...).layer (which becomes an error string), Prisma's nested select results, and Saga.toLayer from 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 inferFromTypes through invokeOnce so the new cache could cover them. invokeOnce also pushes the pair onto the recursion stacks and stops with a circularity when isDeeplyNestedType(..., 2) finds the same recursion identity twice on both sides. The outer Eff<...> and the inner Eff<A, R> share an identity, so the inner pair is reported as a cycle and inferFromTypeArguments never runs for it. Before #64553 those paths called inferFromTypeArguments directly, without the guard, as TypeScript 5.9 and 7.0 still do.

Fix

invokeOnce is split into a worker with a checkRecursion flag. The two type-argument paths go through invokeOnceWithoutRecursionCheck, 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 guarded invokeOnce. The other three callers (conditional types, generic mapped types, object types) are unchanged.

Testing

🤖 Generated with Claude Code

…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>

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

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

Comment on lines +55 to +58
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;

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.

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).

@kennysarnoski-jarvis

Copy link
Copy Markdown
Author

@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>

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.

Regression in 7.1 (#64553): R inferred as unknown through a generic type alias call signature with a nested same-type argument

3 participants

⌂ Proxy