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

Method infers the wrong type when passing a value that is typealiased that uses union and intersection types #55648

Closed
NatanLifshitz opened this issue Sep 6, 2023 · 8 comments
Assignees
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@NatanLifshitz
Copy link

NatanLifshitz commented Sep 6, 2023

🔎 Search Terms

typealias inference, union type inference, intersection type inference, inconsistent inference

🕗 Version & Regression Information

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

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.3.0-dev.20230906#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgISmEwGs0BJFNLXOAbwCgBfBh0SWOdAVyWxgQQkcAEZQIZJAFUAzpgDmwADwBlADRwAogD4AFERncANjABccKUkFWAXmlUadASnrNW7aPBgBPMHkvWCHZQDlracAC8cLoqcAA+Wi4AZAREpBRUGDjA7uCecD5+FlZCQWiyCnhRAaXBSgBuEAgAJhoyMFDI8tqsPHwCQlyYCEbAzZToaMB8wLoujHCLcNhC7XAg5jW25XKKkXCKMJTtmDNzDEtwAPRXcADCABbA2CRDIzJc0HBIEHBpMkILksxBJphVFLoQE43Aw+vxrHBDNhcDIZBMpmd5kDFiskGsNsVAnVGi02h0uuEoodjjBTrhzpcbnAVJgALZ4QrAD6YD4iYDoaB4ETceDYJ4vRHcZHAMYybGicSScGzKEw2G8eGDal42mYzYlbZQZX0JhAA

💻 Code

export interface BreakerInterface {
}

export function brokenUsage<S, E>(result: Unionizer<S, E>) {
}

export type Unionizer<S, E> = (S | E) & BreakerInterface

export type UnionizerUsage = Unionizer<void, string>

function failedInference() {
    const x: UnionizerUsage = getInstance()
    // Check fails for no reason
    brokenUsage(x)
}

function successInference() {
    const x: Unionizer<void, string> = getInstance()
    // Same types as before but check succeeds
    brokenUsage(x)
}


function getInstance(): UnionizerUsage {}

🙁 Actual behavior

Code fails to compile due to "Type 'void & BreakerInterface' is not assignable to type 'string'" which is actually due to the brokenUsage call inferring x as Unionizer<string, string> instead of Unionizer<void, string>.

🙂 Expected behavior

UnionizerUsage is expanded to Unionizer<void, string> and code compiles

Additional information about the issue

No response

@fatcerberus
Copy link

which is actually due to UnionizerUsage being expanded to Unionizer<string, string>

Looking closer, this is not actually the case. UnionizerUsage is indeed equivalent to Unionizer<void, string>. It's the brokenUsage() call that's being inferred differently. Hence the error message:

Argument of type 'UnionizerUsage' is not assignable to parameter of type 'Unionizer<string, string>'.

That said, void in general doesn't behave well outside the context of being the return type of a function (in particular, it's not an alias for undefined). See #42709.

@NatanLifshitz NatanLifshitz changed the title Typealias is expanded into the wrong type in very specific case involving union and intersection types Method infers the wrong type when passing a value that is typealiased that uses union and intersection types Sep 6, 2023
@whzx5byb
Copy link

whzx5byb commented Sep 7, 2023

Since this is a regression between 4.2.0-dev.20210111 and 4.2.0-dev.20210112, I believe this is caused by the same reason as #48070 (comment) says.

@fatcerberus
Copy link

Now that I look at this again, it’s definitely weird that generic inference would infer a type that’s not actually compatible with the value it’s inferring from.

@andrewbranch andrewbranch added the Needs Investigation This issue needs a team member to investigate its status. label Sep 11, 2023
@andrewbranch andrewbranch added this to the TypeScript 5.3.0 milestone Sep 11, 2023
@ahejlsberg
Copy link
Member

ahejlsberg commented Sep 12, 2023

This is an indirect effect of #42284. Before that PR, the declaration

export type UnionizerUsage = Unionizer<void, string>

did not associate a new alias with the type, so the type would always display as Unionizer<void, string>. Following the PR, we re-alias the type so it displays as UnionizerUsage. This also means that we forget about the previous alias Unionizer<void, string>. So now, when inferring from UnionizerUsage to Unionizer<S, E> we can't just infer between the type argument positions. Instead, we perform structural inference between void & BreakerInterface | string & BreakerInterface and S & BreakerInterface | E & BreakerInterface. This structural inference doesn't have the ability to distinguish between S and E because union types aren't ordered, and so we get a different outcome.

Short story, inference between two types with the same type alias is sometimes more precise than inference between the actual types denoted by the aliases, and that's what we're seeing here.

@ahejlsberg ahejlsberg added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Needs Investigation This issue needs a team member to investigate its status. labels Sep 12, 2023
@fatcerberus
Copy link

fatcerberus commented Sep 15, 2023

Short story, inference between two types with the same type alias is sometimes more precise than inference between the actual types denoted by the aliases, and that's what we're seeing here.

Given that the structural nature of the type system is so heavily emphasized, this feels surprising enough that it should probably be covered in documentation. e.g. that sometimes TS can use the high-level structure of a type alias for inference (and other purposes, e.g. variance measurement drives assignability) rather than the type it resolves to.

Hmm, using the structure of the structural types to drive inference. Maybe looks a bit like nominal typing from the outside, but it isn't really that either. Needs a buzzword, methinks. We'll call this new concept... metastructural typing? 😜

@NatanLifshitz
Copy link
Author

In what build is this fixed?
In 5.4.0-dev.20240124, we still get:

Argument of type 'UnionizerUsage' is not assignable to parameter of type 'Unionizer<string, string>'.
  Type 'void & BreakerInterface' is not assignable to type 'Unionizer<string, string>'.
    Type 'void & BreakerInterface' is not assignable to type 'string'.(2345)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

5 participants