You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Method infers the wrong type when passing a value that is typealiased that uses union and intersection types #55648
exportinterfaceBreakerInterface{}exportfunctionbrokenUsage<S,E>(result: Unionizer<S,E>){}exporttypeUnionizer<S,E>=(S|E)&BreakerInterfaceexporttypeUnionizerUsage=Unionizer<void,string>functionfailedInference(){constx: UnionizerUsage=getInstance()// Check fails for no reasonbrokenUsage(x)}functionsuccessInference(){constx: Unionizer<void,string>=getInstance()// Same types as before but check succeedsbrokenUsage(x)}functiongetInstance(): 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
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.
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
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.
This is an indirect effect of #42284. Before that PR, the declaration
exporttypeUnionizerUsage=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.
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? 😜
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)
Activity
fatcerberus commentedon Sep 6, 2023
Looking closer, this is not actually the case.
UnionizerUsageis indeed equivalent toUnionizer<void, string>. It's thebrokenUsage()call that's being inferred differently. Hence the error message:That said,
voidin general doesn't behave well outside the context of being the return type of a function (in particular, it's not an alias forundefined). See #42709.NatanLifshitz commentedon Sep 6, 2023
voidwas just an example of the simplest type. The same happens with any other type, see example withnumberinstead:https://www.typescriptlang.org/play?ts=5.3.0-dev.20230906#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgISmEwGs0BJFNLXOAbwCgBfBh0SWOdAVyWxgQQkcAEZQIZJAFUAzpgDmwADwBlADRwAogD4AFERncANjABccKUkFWAXmlUadASnrNW7aPBgBPMHkvWCHZQDlracAC8cLoqcAA+Wi4AZAREpBRUGDjA7uCecD5+FlZCQWiyCnhRAaXBSkjcALYiaBoyMFDI8tqsPHwCQlyYCEbAACaU6GjAfMC6LoxwS3DYQu1wIOY1tuVyipFwijCU7Ziz8wzLcKPw3lslOyENza1w7Z1I3QcgANxLAPT-OASS7LQFwADCAAtgNgSEMRjIuNA4EgIHA0jIhKClmIJDMKopdCAnG4GH1+NY3txsLgZDJJtNzgscSs1vBNsVAnVni0oG0Ol1wlEjicYGdcBcruCVJhGnhCsAkZgkS10NA8CJuPBsDC4dTacBxjJWXjJIS5iSyeTeJTBqKkKdmfdubtKq4rkQYNwoMIAEzMIA
[-]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[/+]whzx5byb commentedon 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 commentedon Sep 8, 2023
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 commentedon Sep 11, 2023
#48092 doesn’t fix this: https://www.staging-typescript.org/play?ts=5.0.0-pr-48092-16#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgISmEwGs0BJFNLXOAbwCgBfBh0SWOdAVyWxgQQkcAEZQIZJAFUAzpgDmwADwBlADRwAogD4AFERncANjABccKUkFWAXmlUadASnrNW7aPBgBPMHkvWCHZQDlracAC8cLoqcAA+Wi4AZAREpBRUGDjA7uCecD5+FlZCQWiyCnhRAaXBSgBuEAgAJhoyMFDI8tqsPHwCQlyYCEbAzZToaMB8wLoujHCLcNhC7XAg5jW25XKKkXCKMJTtmDNzDEtwAPRXcADCABbA2CRDIzJc0HBIEHBpMkILksxBJphVFLoQE43Aw+vxrHBDNhcDIZBMpmd5kDFiskGsNsVAnVGi02h0uuEoodjjBTrhzpcbnAVJgALZ4QrAD6YD4iYDoaB4ETceDYJ4vRHcZHAMYybGicSScGzKEw2G8eGDal42mYzYlbZQZX0JhAA
ahejlsberg commentedon Sep 12, 2023
This is an indirect effect of #42284. Before that PR, the declaration
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 asUnionizerUsage. This also means that we forget about the previous aliasUnionizer<void, string>. So now, when inferring fromUnionizerUsagetoUnionizer<S, E>we can't just infer between the type argument positions. Instead, we perform structural inference betweenvoid & BreakerInterface | string & BreakerInterfaceandS & BreakerInterface | E & BreakerInterface. This structural inference doesn't have the ability to distinguish betweenSandEbecause 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.
fatcerberus commentedon Sep 15, 2023
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 commentedon Jan 25, 2024
In what build is this fixed?
In 5.4.0-dev.20240124, we still get: