Skip to content

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

Closed

Description

@NatanLifshitz

🔎 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

Activity

  1. fatcerberus commented on Sep 6, 2023

    @fatcerberus

    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.

  2. 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[/+] on Sep 6, 2023
  3. whzx5byb commented on Sep 7, 2023

    @whzx5byb

    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.

  4. fatcerberus commented on Sep 8, 2023

    @fatcerberus

    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.

  5. ahejlsberg commented on Sep 12, 2023

    @ahejlsberg
    Member

    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.

  6. added
    Design LimitationConstraints of the existing architecture prevent this from being fixed
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Sep 12, 2023
  7. fatcerberus commented on Sep 15, 2023

    @fatcerberus

    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? 😜

  8. NatanLifshitz commented on Jan 25, 2024

    @NatanLifshitz
    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

Metadata

Metadata

Labels

Design LimitationConstraints of the existing architecture prevent this from being fixed

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions