Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistent inference of generic function signature #57373

Closed
rotu opened this issue Feb 11, 2024 · 5 comments
Closed

Inconsistent inference of generic function signature #57373

rotu opened this issue Feb 11, 2024 · 5 comments
Assignees
Labels
Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@rotu
Copy link

rotu commented Feb 11, 2024

πŸ”Ž Search Terms

infer, generic, function

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about Generics

⏯ Playground Link

https://www.typescriptlang.org/dev/bug-workbench/?ts=5.4.0-dev.20240211#code/C4TwDgpgBA4hB2AxArvKBeKAeAGgGgD4AKADwC4cBKdAnAKDtEigEEBGDWBFNCE4BABMAzlCKkyw4ACcAlvADm1AlLmLKUAPxQZyaGSgAzAIYAbYdAD0lndL11rUJwD1NjcNBYAmTnCSooPgF4ETEJVXklGnlDCGkoAC0NbQSoA3hkU1MoR1QAa3gAewB3eDwoYQALQszBKAAjaAjFBxsXNyZPAGZfbgCgoVFxchi4xOVmpS1EtKgMrJybedNyqprTOsaKmUjWpyhXIA

πŸ’» Code

type GenFun = <X,>(x:X)=>X

type A1 = GenFun extends ((x:string)=>string) ? true : false // true
//   ^?
type A2 = GenFun extends ((x:string)=>infer Z) ? Z : null // unknown, should be string
//   ^?
type A3 = GenFun extends ((x:infer Z)=>string) ? Z : null // null, should be string
//   ^?

πŸ™ Actual behavior

type A1 = true
type A2 = unknown
type A3 = null

πŸ™‚ Expected behavior

I expect A2 and A3 to be string, since this is the instantiation of the type variable which satisfies the extends relation.

Additional information about the issue

No response

@rotu rotu changed the title Inconsistent inference inside generic function Inconsistent inference of generic function signature Feb 11, 2024
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Feb 12, 2024
@ahejlsberg
Copy link
Member

The behavior here is an effect of how we perform inference. The steps are:

  • Infer from GenFun to (x: string) => infer Z in order to produce inferences for Z.
  • Since GenFun is generic, we end up inferring the constraint of X, i.e. unknown.
  • Instantiate the extends type, producing (x: string) => unknown.
  • Infer from (x: string => unknown) to (x: X) => X in order to produce inferences for X.
  • This produces the inference string for X.
  • Since (x: string) => string extends (x: string) => unknown, return the instantiated true type, i.e. unknown.

In an ideal world we'd perform full unification and realize that string and Z are unified through X, but that's not how things work--and full unification would bring about all sorts of other challenges.

So, this is effectively working as intended.

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Feb 19, 2024
@rotu
Copy link
Author

rotu commented Feb 20, 2024

Hmm... That makes sense why, though it seems like slightly inappropriate behavior.

I don't expect full unification, but I do expect infer to resolve any constraint that extends would recognize.

In particular, the premature resolution of X you describe above seems to violate the description of this feature in 2.8 release notes (is there a better normative description of how extends should work?). I would think that the type variable X is deferred and, in example A3, infer Z is in a contravariant position, which should mean that inference starts with string and accumulates by intersection.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 23, 2024
@ahejlsberg
Copy link
Member

Regarding deferral of conditional type resolution, the 2.8 release notes state

A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables.

The phrase depends on one or more type variables here refers to type variables that are declared in an outer enclosing scope, such as type parameters of a type alias for the conditional type. It specifically doesn't refer to infer type variables declared in the conditional type itself or type parameters declared by function types in the left or right extends positions of the conditional type. Those all need to be resolved by inference since there's no way for instantiations of the conditional type to explicitly specify them.

In the original issue example here, all of the type variables are internal to the conditional type and won't cause deferral. Instead, those type variables have to be resolved though inference, which in TypeScript's case always works in one direction or another. Here, as I explained above, we first infer from the check type to the extends type to produce inferences for infer type variables. Then, if necessary, we infer from the instantiated extends type back to the check type to produce inferences for type parameters of generic function types. This algorithm isn't perfect because it doesn't unify type variables, but it avoids some of the complications of unification (such as implications of unification and subtyping).

@rotu
Copy link
Author

rotu commented Feb 23, 2024

Gotcha. I imagined that infer meant "find a value for T such that the relation holds or show that the relation cannot be made to hold" and the "outside-in" algorithm in use is not quite that.

Since inference proceeds from the constraint, I also came up with these examples which should work a bit better with inference:

// note: identical definitions
type GenFun2<T> = <S extends T>(x:S)=>S
type GenFun3<T> = <S extends T>(x:S)=>S

type B1 = GenFun2<string> extends GenFun2<string> ? true : false // true
//   ^?

// even if incompatible types are given this is true (!)
type B1_1 = GenFun2<string> extends GenFun2<number> ? true : false // true, expected false
//   ^?

// expect B2 and B3 to be the same, either unknown or null
type B2 = GenFun2<unknown> extends GenFun2<infer T> ? T : null // unknown
//   ^?
type B3 = GenFun2<unknown> extends GenFun3<infer T> ? T : null // unknown
//   ^?

// expect B4 and B5 to be the same, either object or null
type B4 = GenFun2<object> extends GenFun2<infer T> ? T : null // object
//   ^?
type B5 = GenFun2<object> extends GenFun3<infer T> ? T : null // null
//   ^?

Workbench Repro

I'm surprised B5 fails, and I'm thinking that B3 works only by some implementation accident.

Edit: added B1_1 which demonstrates (I think) that GenFun2<T> is incorrectly deduced as being independent of T.

Maybe this is #53210?

@typescript-bot typescript-bot added the Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros label Feb 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants