microsoft / microsoft/TypeScript

Surprising difference in nested calls inference from contextual type

Open
#56,714 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Domain: check: Type Inference Help Wanted Possible Improvement
Dominant language
Go
Stars
111k
Forks
14.3k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

🔎 Search Terms

inference nested contextual type reverse mapped type parameter

🕗 Version & Regression Information
  • This is the behavior in every version I tried
⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.4.0-dev.20231208#code/C4TwDgpgBAggxsA9gJwEoQGYB4AqBJAOzAFdgA+KAXigG8AoKKOZCAQ2AgH4AuKACgCWRUr3zDgASioUAbogEATANx0AviroKIcADasWUDMQIIBiAoeSIAtgGEW7CLkIlyfBkwcdeg8aJekUpSy8goANHQSvPBIaJjO4mQaWrr60EYmwGYWAM4QwMRgAIxYHjgxKDlQEAAeHAQKVehwKApYOcDIQgDmYbAIKOjYBBAyEMhkZBFkgtZgOjm89IysA8g5PLQejADaANJQQlAA1hAgiBhQ5Ws5ALqiFev7tyqM6mpRV485Gi0EHVAWDliDpgEUqFA8gVinxllBVrFFltGIwAO4CYAACwCwF4GCsdi8ED4vlcvA6XQI3SCFBoqgkERRUHRWMQpBxeIJ9jYHBJNNo9MZUFUEXpGiBILBKgA9NKoAA9Th0TTaPQGDKmcyQ-KFABMpUY10R1TqEAaTW0rXanR6fUeQywIzGEymdBmAjmCyW8JumyNlWFn3961+5gBEtBuohUL1sI8CMqS22zIx2L8lhs3McJKEZMhNqp-LpDOTLMxbOAHIzhJ5xL4RcFHhFH3FEGBkZlcsVdCAA

💻 Code
type ActorRef<TInput> = {
  create?: (input: TInput) => void;
};

declare function fromCreate<TInput>(
  create: (input: TInput) => void,
): ActorRef<TInput>;

declare function setup1<
  TActors extends Record<string, ActorRef<never>>,
>(impls: {
  actors?: {
    [K in keyof TActors]: TActors[K];
  };
}): TActors;

const result1 = setup1({
  actors: {
    withInput: fromCreate((input: string) => {}),
    withoutInput: fromCreate(() => {}),
  },
});

result1;
// ^? const result1: { withInput: ActorRef<string>; withoutInput: ActorRef<unknown>; }

declare function setup2<
  TActors extends Record<string, ActorRef<never>>,
>(impls: { actors?: TActors }): TActors;

const result2 = setup2({
  actors: {
    withInput: fromCreate((input: string) => {}),
    withoutInput: fromCreate(() => {}),
  },
});

result2;
// ^? const result2: { withInput: ActorRef<string>; withoutInput: ActorRef<never>; }
🙁 Actual behavior

The only difference between those 2 calls is that TActors of the outer setup call is either iterated over (with Identity-like mapped type) or not.

Yet when this "noop" iteration is involved we infer the constraint of the TInput parameter in the nested fromCreate call. When the iteration is not involved then we infer never.

🙂 Expected behavior

I'd expect both of those calls to behave identically.

Additional information about the issue
  1. without the iteration the contextual type for this inner call is ActorRef<never> whereas with the iteration it's TActors["withoutInput"]
  2. without the iteration we successfully inferTypes using InferencePriority.ReturnType from the instantiated contextualType but with the iteration we get silentNeverType for TActors and thus also silentNeverType for the whole indexed access. A silentNeverType is non-inferrable
  3. One of those calls can gather inference candidates (and thus ActorRef<never> gets inferred) and the other one is not (so it defaults to its constraint).

It might not be worth fixing this. I figured out that it might still be interesting to raise this. Maybe a rule similar to the one introduced here could be introduced somewhere? A more specific type (depending on the variance of the type parameter) could be picked up from the constraint type and the type inferred using InferencePriority.ReturnType. I'm not sure if that would work in practice though - just thinking out loud while writing, without giving this more thought 😅

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked TypeScript Playground reproduction and compare setup1 with setup2, focusing on the contextual types and inferred result types shown in the issue. Trace the mentioned inferTypes, InferencePriority.ReturnType, contextualType, and silentNeverType paths, and review the rule introduced in PR 56506. Done means both calls infer equivalent ActorRef types without regressing related inference behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.