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

Possible regression in 3.4 with distributive conditional types #30569

Closed
dragomirtitian opened this issue Mar 24, 2019 · 6 comments · Fixed by #30592
Closed

Possible regression in 3.4 with distributive conditional types #30569

dragomirtitian opened this issue Mar 24, 2019 · 6 comments · Fixed by #30592
Assignees
Labels
Bug A bug in TypeScript

Comments

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Mar 24, 2019

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.

@zpdDG4gta8XKpMCd
Copy link

can you explain why you do this:

type UnionKeys<T> = T extends any ? keyof T : never;

instead of just

type UnionKeys<T> = keyof T;

@dragomirtitian
Copy link
Contributor Author

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

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Mar 24, 2019

control over distributivness need its own syntax, otherwise it looks like black magic, sorry for derailing

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 25, 2019

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.

@dragomirtitian
Copy link
Contributor Author

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

@RyanCavanaugh RyanCavanaugh self-assigned this Mar 25, 2019
@ahejlsberg ahejlsberg added this to the TypeScript 3.4.0 milestone Mar 25, 2019
@weswigham
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants