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

Probable bug: Can't infer an invariant generic on contravariant position when involving another generic #45255

Closed
devanshj opened this issue Jul 31, 2021 · 4 comments Β· Fixed by #48584
Labels
Fix Available A PR has been opened for this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@devanshj
Copy link

devanshj commented Jul 31, 2021

Bug Report

πŸ”Ž Search Terms

invariant generic, contravariant inference
Maybe related or perhaps because of the same design limitation mentioned in #44999

πŸ•— Version & Regression Information

Tested with 4.3.5

⏯ Playground Link

Playground

πŸ’» Code

declare const d: <T, U>(_: { v: T, m: (t: T) => U, f: (u: U) => void }) => void
declare const a: "a"
declare const b: "b"

d({
  v: a,
  m: t => t,
  f: u => {
    let test1: "a" = u // Type 'unknown' is not assignable to type '"a"'.
  }
})

d({
  v: a,
  m: (t: "a") => t,
  f: u => {
    let test2: "a" = u
  }
})

d({
  v: a,
  m: _ => b,
  f: u => {
    let test3: "b" = u // Type 'unknown' is not assignable to type '"b"'
  }
})

d({
  v: a,
  m: () => b,
  f: u => {
    let test4: "b" = u
  }
})

πŸ™ Actual behavior

In 1st and 3rd function call U gets inferred to unknown

πŸ™‚ Expected behavior

In 1st function call U should get inferred to "a" and in 3rd function call U should get inferred to "b".

Clearly the contravariant position of U in m seems to be a problem because if we annotate it (as done in 2nd call) or omit it (as done in 4th call) it gets inferred to what is expect and there are no error.

A real world use case:

declare const branch:
  <T, U extends T>(_: { test: T, if: (t: T) => t is U, then: (u: U) => void }) => void

declare const x: "a" | "b"

branch({
  test: x,
  if: (t): t is "a" => t === "a",
  then: u => {
    let test1: "a" = u // Type '"a" | "b"' is not assignable to type '"a"'
  }
})

branch({
  test: x,
  if: (t: "a" | "b"): t is "a" => t === "a",
  then: u => {
    let test2: "a" = u // compiles
  }
})
@RyanCavanaugh
Copy link
Member

Inference can do multiple "rounds" based on parameters, but this isn't applied to object properties (since they are not bounded in size the way parameters are). See #30134

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Aug 5, 2021
@devanshj
Copy link
Author

devanshj commented Aug 5, 2021

Ah okay *goes and does a πŸ‘πŸ»*

Unrelated: I always thought that the inference does "rounds" (especially when working with those T extends M<T> types) good to know that's actually how it happens :P

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript Fixed A PR has been merged for this issue and removed Design Limitation Constraints of the existing architecture prevent this from being fixed labels Apr 6, 2022
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 4.7.0 milestone Apr 6, 2022
@DanielRosenwasser
Copy link
Member

According to @Andarist this should be fixed in 4.7.

@devanshj
Copy link
Author

devanshj commented May 1, 2022

#48538 does fix the issue, but partially. For example I'd expect this to compile too...

declare const branch:
  <T, U extends T>(_: { test: T, if: (t: T) => t is U, then: (u: U) => void }) => void

declare const is:
  <T, U extends T>(u: U) => (t: T) => t is U

declare const x: "a" | "b"

branch({
  test: x,
  if: is("a" as "a"),
  then: u => {
    let test: "a" = u // should compile, doesn't
  }
})

branch({
  test: x,
  if: (t): t is "a" => t === "a",
  then: u => {
    let test: "a" = u // should compile, does
  }
})


declare const pa:
  <T, U extends T>(t: T, p: (t: T) => t is U) => t is U

if (pa(x, is("a" as "a"))) {
  let test: "a" = x // should compile, does
}

So can we reopen this? Or should I submit a new issue? (And even if this is a design limitation then maybe it's a good idea to open an issue to track it)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants