Repository navigation
Regression in 7.1 (#64553): R inferred as unknown through a generic type alias call signature with a nested same-type argument #64757
Description
Note
🤖 Claude Opus 5.5 writing on behalf of Kenny
🔎 Search Terms
inference unknown, generic type alias call signature, generator, invokeOnce, isDeeplyNestedType, #64553, cache inferences type arguments, regression 7.1
🕗 Version & Regression Information
- This changed between
7.1.0-dev.20260929.1and7.1.0-dev.20261001.1. - It is still present in
7.1.0-dev.20261012.1(next). 7.0.2and5.9.3infer the expected type.- The tsc-rs maintainers bisected the Go build to c8e9b7d, Cache inferences made from type arguments #64553 ("Cache inferences made from type arguments"). The parent commit gives the expected type. Details: 0.4.0 regression: Effect
HttpRouter.middleware(...).layertypes as string (requires inferredunknown); 0.3.0 and tsc 7.1.0-dev accept pingdotgg/ts-rust#79 (comment)
⏯ Playground Link
No playground link. The code below is self-contained, with no dependencies.
💻 Code
interface Effect<out A, out R = never> {
readonly _A: () => A;
readonly _R: () => R;
[Symbol.iterator](): Iterator<Effect<A, R>, A>;
}
declare function succeed<A>(value: A): Effect<A>;
declare function gen<Eff extends Effect<any, any>, AEff>(
body: () => Generator<Eff, AEff, never>,
): () => Effect<AEff, Eff extends Effect<infer _A, infer R> ? R : never>;
interface Req { readonly url: string }
declare const req: Effect<Req, Req>;
type Make<P> = <R>(m: Effect<() => Effect<string, R>>) => [R];
declare const make: Make<unknown>;
const r = make(succeed(gen(function* () {
yield* req;
return "";
})));
const check: [1] = r; // error message shows what R was inferred astsconfig.json: { "compilerOptions": { "target": "ES2023", "module": "NodeNext", "strict": true, "noEmit": true } }
| Version | r |
|---|---|
| 5.9.3 | [Req] |
| 7.1.0-dev.20260929.1 | [Req] |
| 7.1.0-dev.20261001.1 | [unknown] |
| 7.1.0-dev.20261012.1 | [unknown] |
🙁 Actual behavior
R is inferred as unknown, so r is [unknown].
🙂 Expected behavior
R is inferred as Req, so r is [Req], as in 5.9.3, 7.0.2 and 7.1.0-dev.20260929.1.
Additional information about the issue
The tsc-rs maintainers traced it to the depth-2 nesting check in invokeOnce (isDeeplyNestedType(..., 2), about line 363 of inference.go). #64553 now sends the Effect/Effect pair through it. For Effect<() => Effect<A, R>>, the check treats the inner pair as a cycle and infers nothing for the inner R. It only happens when the signature comes from an instantiated generic type alias (Make<P> above). Calling a plain generic function with the same shape infers Req.
Real-world impact: this is the shape of Effect 4's HttpRouter.middleware<...>()(...), whose signature is the generic alias middleware.Make<Provides, Handles>. A middleware written with Effect.fn or Effect.gen that reads the request gets R = unknown, and .layer resolves to the string type "Need to .combine(middleware) that satisfy the missing request dependencies". In one Effect app this cascades into 145 errors.
Activity
jclaessens97 commentedon Oct 21, 2026
Another shape of this regression, from Prisma's nested
selectresults (findUnique/findManywith relations two levels deep). Same boundary: fine on 6.0.3, 7.0.2 and7.1.0-dev.20260929.1;unknownfrom7.1.0-dev.20261001.1through7.1.0-dev.20261020.3. Here the generic alias is a parameter type, not a call signature; the same type written inline still infers.tsc --strict --noEmit repro.ts, no other options needed.MinlineMvia alias{ body: string }{ body: string }{ body: string }{ body: string }{ body: string }{ body: string }{ body: string }unknown(TS2571){ body: string }unknown(TS2571)One level of nesting through an alias (
type OneLevel<M> = { messages: Array<M> }, called withthread.conversations[0]) still infers{ body: string }on every version.kennysarnoski-jarvis commentedon Oct 21, 2026
Note
🤖 Claude Opus 5.5 responding on behalf of Kenny
Thanks for the second repro. I ran it locally (
--strict --noEmit, unmodified) and got the same boundary:typescript@5.9.3typescript@7.1.0-dev.20260929.1typescript@7.1.0-dev.20261001.1viaAliastypescript@7.1.0-dev.20261020.3viaAliasThe original report shows up via Effect's
HttpRouter.middleware(...)(Rbecomesunknownthrough themiddleware.Makealias). This one hits Prisma'sGetFindResultwith a generic alias as a parameter type. In both, inference fails only when the type goes through an instantiated generic alias at two levels of nesting, and both start with #64553.Downstream, the regression also reaches tools that pin a 7.1 nightly. For example,
tsc-rs0.4.0+ (pingdotgg/ts-rust#79) shows it on both repros, and 0.3.0, which still pins 20260929.1, does not.artile commentedon Oct 31, 2026
Note
🤖 Claude Opus 5.5 writing on behalf of a TypeScript 7.1 user. All results below come from local runs.
Third real-world shape, with two details that may help:
Fis declared but not used in the parameter, inference works.The boundary is also narrower:
7.1.0-dev.20260930.4passes, and7.1.0-dev.20261001.1fails.7.1.0-betaand7.1.0-dev.20261031.1still fail.tsconfig.json:{ "compilerOptions": { "strict": true, "noEmit": true, "target": "esnext", "module": "esnext", "moduleResolution": "bundler", "skipLibCheck": true, "lib": ["esnext"] }, "files": ["a.ts"] }interface S<F>method, nestedEff<F, Req>neverneverunknownunknowntype Make<F> = <Req>(...), nestedEff<F, Req>neverneverunknownunknowninterface S<F>method,Fnot used in the parameternevernevernevernevernevernevernevernevernevernevernevernever<F, Req>,Finferred from another argumentneverneverneverneverWith a real requirement in the generator body, 7.0.2 and
20260930.4infer that requirement, and20261001.1and later inferunknown.Real-world impact: this is
Saga.toLayerof@alchemy.run/domain, called with a handler fromEffect.fn. In one app, 3 sagas gave 137 TypeScript errors and 285 false Effect diagnostics. A workaround for this shape: return(payload) => run(payload)from the build effect, whererunis theEffect.fnresult.kennysarnoski-jarvis commentedon Oct 31, 2026
Note
🤖 Claude Mythos 5.1 responding on behalf of Kenny
Kenny and I (Claude, Mythos 5.1) tracked this down and opened #65077 with a fix and a compiler test that covers the three shapes in this thread.
Cause: #64553 routed inference from the type arguments of two references to the same generic type (or alias) through
invokeOnce. Besides caching,invokeOncepushes the pair onto the recursion stacks and reports a circularity whenisDeeplyNestedType(..., 2)finds the same recursion identity twice on both the source and the target side. ForEff<() => Eff<A, R>, never>the outer and the inner reference share an identity, so the inner pair is treated as a cycle,inferFromTypeArgumentsnever runs for it, andRfalls back tounknown. Before #64553 that path had no recursion guard, and 5.9 and 7.0 still run it without one. That matches the boundary reported above (#64553 is in7.1.0-dev.20261001.1, not in20260930.4).The fix keeps the cache for those two paths and skips only the recursion check. With it, the alias call-signature repro, the Prisma nested
selectshape, theSaga.toLayerrepro and Effect'sHttpRouter.middleware(...).layerall infer as on 7.0.2, andgo test ./...passes.Cover the Prisma shape of microsoft#64757 with the reported repro