TypeScript Version: 3.4.0-dev.201xxxxx
Search Terms:
Code
type UnionKeys<T> = T extends any ? keyof T : never;
type BugHelper<T, TAll> = T extends any ? Exclude<UnionKeys<TAll>, keyof T> : never
type Bug<TAll> = BugHelper<TAll, TAll>
type R = Bug<{ a : any } | { b: any }> // "a" | "b" in 3.3, never in 3.4
Expected behavior:
R should be "a" | "b" (as it previously was in 3.3)
Actual behavior:
R is never in 3.4
Playground Link: link
Related Issues: Probably cause by #29437, #30489
The code above is a simplification of the StrictUnion type I posted on SO, and this issue was reported there as a comment. The type was also included by another SO user in SimplyTyped
Workaround
For the StrictUnion type a simple workaround is to pass in union keys to StrictUnionHelper. This will achieve the same result as in 3.4:
type StrictUnionHelper<T, TAllKeys extends PropertyKey> = T extends any ? T & Partial<Record<Exclude<TAllKeys, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, UnionKeys<T>>
If breaking change is intended, I would like some guidance on whether the workaround version is likely to remain stable in the next releases or if there is a fundamental issue with the idea behind the type.
can you explain why you do this:
type UnionKeys<T> = T extends any ? keyof T : never;
instead of just
type UnionKeys<T> = keyof T;
@aleksey-bykov Because I want a union of all possible keys in a union. For example keyof ({a: any} | {b : any }) is "a" & "b" (I would have expected never, as in 2.8 but in practical terms that intersection seems close to never).
Using the distributive behavior of conditional types, UnionKeys gets the union of keys in each member of the union and the result is a union of all possible keys. So UnionKeys<{a: any} | {b : any }> is "a" | "b". With this union we can then do other interesting things, like find the keys that are possible in a union but not part of a given member.
control over distributivness need its own syntax, otherwise it looks like black magic, sorry for derailing
The condition T extends any in BugHelper is creating a substitution type for T with any in the true branch. This causes the true type Exclude<UnionKeys<TAll>, keyof T> to reduce to never because the constraint of UnionKeys<TAll> is always assignable to keyof any.
Changing the conditional to use unknown should correct this because keyof unknown = never.
T extends unknown ? Exclude<UnionKeys<TAll>, keyof T> : never
I'm not sure what merit there is in creating a substitution type with any, or whether it is that simple to disable. I think while people use conditional types to essentially map over unions there should be some semi-canonical way to do this. IMO, unknown is better than any.
@jack-williams After posting I found some of the existing GH issues. I still find this is a pretty big breaking change. Back in 2.8 when conditional types were introduced, any seemed like a good choice to use to just get the distributive part of conditional types, and quite frankly I have used it a lot, both in actual code and in several SO answers (which I'm gonna have loads of fun trying to track down and correct)
I know any is special but this new behavior just seems to add another asterisk to an already difficult to understand TS feature. Conditional types distribute over naked type parameters ... sometimes.. unless the condition involves any. One upside is that since it is a pretty arcane feature, probably not a lot of people will be impacted.
Also I would kindly ask someone on the team (@RyanCavanaugh) to document this somewhere. It is not mentioned in the RC announcement or in the Breaking changes section. I'm not sure if it would have helped me but it would be useful to have some official docs to point to if it comes up in other contexts.
@jack-williams tracked it down almost exactly - in getBaseConstraintOfType we were using type.substitute unguarded as the constraint of a substitution type (whereas elsewhere we try to be more any aware and avoid substituting with any, since that deletes information and introduces unsoundness, yet in context only implies the same as unknown).
I'm not sure what merit there is in creating a substitution type with any, or whether it is that simple to disable
There is not, IMO - they only ever delete important information. My fix will be modifying the internal constructor such that substitutions which substitute any or unknown cease to be substitutions and are simply the underlying type variable. That should simplify a lot of our handling around them~
Most helpful comment
@jack-williams tracked it down almost exactly - in
getBaseConstraintOfTypewe were usingtype.substituteunguarded as the constraint of a substitution type (whereas elsewhere we try to be moreanyaware and avoid substituting withany, since that deletes information and introduces unsoundness, yet in context only implies the same asunknown).There is not, IMO - they only ever delete important information. My fix will be modifying the internal constructor such that substitutions which substitute
anyorunknowncease to be substitutions and are simply the underlying type variable. That should simplify a lot of our handling around them~