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
Mapped Types should distribute over union types #28339
This might be a bug or an intentional consequence of the original design, but mapped types do not distribute over union types. The example below demonstrates what happens:
typeFoo={x: false;foo?: string}typeBar={x: true;foo: string}typeFooOrBar=Foo|BartypeMappedFooOrBar=Pick<FooOrBar,keyofFooOrBar>// the type of MappedFooOrBar will be { x: boolean; foo: string | undefined }// note how foo is now required even with x = false// and also how you're now allowed to set foo = undefined even with x = true
Use Cases
Higher order components that receive components whose props are union types.
This usually is not a problem but it is a problem if you are going to use Pick (or Omit) on their props. This is already a problem with react-redux's connect for example.
Examples
typeFoo={x: false;foo?: string}typeBar={x: true;foo: string}typeFooOrBar=Foo|Bar// input is a union type so this should distributetypeMappedFooOrBar=Pick<FooOrBar,keyofFooOrBar>// should be equivalent to a MappedFooOrBar that distributes over its inputtypeMappedFooBar=Pick<Foo,keyofFooOrBar>|Pick<Bar,keyofFooOrBar>
Checklist
My suggestion meets these guidelines:
This wouldn't be a breaking change in existing TypeScript / JavaScript code
It probably is breaking but is a "good break" as far as I can tell.
This wouldn't change the runtime behavior of existing JavaScript code
This could be implemented without emitting different JS based on the types of the expressions
This isn't a runtime feature (e.g. new expression-level syntax)
Specifically the constraint part which is essentially the input to the map.
// Partial: [P in keyof T], could be read as a function T => [P in keyof T]
// Pick: [P in K], could be read as a function K => [P in K]
Partial is a parametric function on objects T, so applying it uniformly to object types in a union makes sense. Pick is a function on keys; the notion there was ever union of object types has been lost.
Reacted by Titian Cernicova-Dragomir, Nicu Chiciuc, Hayden, Alex Arro, Vyacheslav and Jacob M-G Evans
The difference seems natural if you look at the definitions of Partial and Pick, to me at least.
That's kind of my issue here: Should I actually have to look at the implementation and then infer different behavior? Or wouldn't it be nicer if they behave the same since they're both classified as mapped types?
Do you have an example where the current behavior of Pick for unions is useful?
Is this maybe more of an issue with keyof for unions? In a perfect world the keyof { kind: 1; error: string } | { kind: 2; payload: any } would be ('kind' | 'error') | ('kind' | 'payload'). Typescript would not flatten this union and keep the information that 'kind' is the discriminant. Maybe this touches to much internals which is why I would prefer the standard lib Pick to use conditionals to distribute over union types.
At least the title should be changed from "Mapped types" to "Mapped types that are a function of keys". Otherwise it is misleading that all mapped types do not distribute over unions.
The difference seems natural if you look at the definitions of Partial and Pick, to me at least.
That's kind of my issue here: Should I actually have to look at the implementation and then infer different behavior? Or wouldn't it be nicer if they behave the same since they're both classified as mapped types?
Distribution does not happen in all cases for conditional types either. If the type parameter is not used nakedly it will not distribute so you have to know a bit about the type to know if it distributes over unions.
I think we can easily create a distributive version of Pick with what the language currently offers. I don't think it is of as general a use as the original version, but it would look something like:
Reacted by Sebastian "Sebbie" Silbermann, Leon Adler, Homa Wong, Chris Talman, Dacri Burdan, Irakli Safareli, chun shang, Vyacheslav, SJK, Mattias Buelens and 2 more
The problem is that I do suspect changing it now would break some code somewhere. Also the distributive version is considerably more complex, it will slow down compilation and I think in most cases Pick is not used over unions so the people who want it can use custom type such as the one above.
Well many libraries use Pick (see original Post: react higher-order components in particular). In those cases Pick should be agnostic to the type of the type.
I usually use Pick on non-union types.
Can you show me some examples where you use Pick? I'm biased towards react and we use Pick mostly in HOCs which is why the current behavior is so frustrating.
it will slow down compilation
Is there some accepted method to benchmark tsc compilation? I'm not familiar with all the intricacies of the language server. Can I just repeatedly run the cli or do I need to reset some things in between?
That's kind of my issue here: Should I actually have to look at the implementation and then infer different behavior? Or wouldn't it be nicer if they behave the same since they're both classified as mapped types?
Yes, I probably agree that the difference between homomorphic (Partial) and non-homomorphic (Pick) mapped types should be better documented. This does not mean the correct solution is to remove the differences.
Fundamentally I think they are mapping over different things and it doesn't really make sense to force them to behave the same. If you look at the implementation of Pick I'm not even sure how you would implement it as a mapped type that distributes over unions; the object type does not appear in the constraint type ([K in …]).
Do you have an example where the current behavior of Pick for unions is useful?
They are useful to me in so far as they work as I expect them to. This isn't a very satisfactory answer, but I've never felt the need for them to distribute.
In a perfect world the keyof { kind: 1; error: string } | { kind: 2; payload: any } would be ('kind' | 'error') | ('kind' | 'payload').
This will fundamentally break union types because they should be associative. If it doesn't break the correctness of the typechecker, I suspect it will at least cause a non-trivial perf hit.
The crux of the issue, IMO, is that distributing non-homomorphic mapped types over unions is incredibly shaky. They are functions that depend on keys, and the keys change if you view as a union or if you distribute.
As an example, consider the distributive implementation of Pick.
For JustPayload, distributing payload to the branch with kind "C" produces an empty object type {}, and consequently the overall type of JustPayload is essentially {}. I'm not sure this is what someone would want or expect.
JustPayload can be useful with the apropriate type guards (an in type guard should narrow the {} away). But I do agree variations in behavior may be required on what you are trying to achieve.
For JustPayload, distributing payload to the branch with kind "C" produces an empty object type {}, and consequently the overall type of JustPayload is essentially {}.
I've slightly lost track of where this proposal is going so I'm recapping, mostly for my own benefit. There seem to be three options:
The initial proposal of have having the Pick mapped type innately distribute. I'm not sure this is even possible given how the type is defined.
The other two options seem to be oriented around redefining the library definition of Pick.
Redefine a 'safe' distributive Pick that still enforces the keyof constraint on the union.
Redefine distributive Pick that lets you pick from a key that only exists in some (or none) of the types.
Option (3) will systematically produce unions with {} in; this seems like pathological behaviour to me. Getting any use out of a union with {} relies on avoiding subtype reduction, and depends upon the unsound in operator. The reasoning is happening at the level of syntax, rather than the model of types.
Options (2) and (3) introduce generic conditional types into the definition. This will further complicate type relations between generic Pick types. Before it only relied upon generic mapped types, now it needs to reason at the level of generic conditional types too. The latter are much harder to get right and still have edge cases. I wouldn't be surprised if #27697 meant that a lot of Pick2 types people think should be assignable to each other are not.
There is also the issue of making users have to juggle the syntax of T extends unknown ? … to understand what should be a simple Pick type. This feels like quite a high cost to me.
Option (3) will systematically produce unions with {} in; this seems like pathological behaviour to me. Getting any use out of a union with {} relies on avoiding subtype reduction, and depends upon the unsound in operator. The reasoning is happening at the level of syntax, rather than the model of types.
Isn't that already the case for non-union types and Pick currently?
I'm not sure I understand how a {} type would be problematic at runtime. Could you add some code that would cause trouble at runtime with a distributive Pick that the current Pick would not also cause with non-union types?
I'm not sure I understand how a {} type would be problematic at runtime. Could you add some code that would cause trouble at runtime with a distributive Pick that the current Pick would not also cause with non-union types?
My concern is not that much about runtime; it's more the meaning of the types produced. A union of object types with {}, such as {} | { foo: string } | { bar: number }, really means the same thing as just {}. Sometimes TypeScript will keep the syntax of the type around, and you can narrow using in, but sometimes TypeScript will reduce the type to its true meaning. For example:
functionfoo(both: {a: string}|{}){if("a"inboth){both// narrowed: { a: string};}// both2: {} because of subtype reductionconstboth2=Math.random()>0.5 ? both : both;if("a"inboth2){both2// never;}}
In the first if the union type is not reduced, so narrowing by in works. However, when TypeScript constructs a union type for both2 it will apply subtype reduction that reduces { a: string } | {} to {}; now narrowing by in does not work.
The difference here, to me, is largely a syntactic artefact.
A Pick type that by default does not put constraints on the keys is going to be more likely to produce union types with {}. These union types might work as a user expects in some cases, but if union reduction is applied then it will break their code. TypeScript now has to be more careful about applying union reductions that should be semantically fine, but in practice could break a lot of user code.
My comments here are largely technical. I think there is a good argument to be data-driven about this and see what happens in the wild. Anecdotally, I've never had a problem with Pick so I don't find the motivation that strong---but I'm a single data point.
Reacted by Titian Cernicova-Dragomir and AnyhowStep
Desirability of distributivity/mapping is entirely scenario-dependent; current defaults exist to a) do the right thing "most" of the time and b) allow opting in to the other behavior
The defaults here only seem "bad" if you ignore all the cases where they work like you expect them to
Desirability of distributivity/mapping is entirely scenario-dependent; current defaults exist to a) do the right thing "most" of the time and b) allow opting in to the other behavior
The defaults here only seem "bad" if you ignore all the cases where they work like you expect them to
Ryan Cavanaugh (@RyanCavanaugh) I mostly see your point here that the existing Pick implementation works for most cases and that users can opt in to the more complex version of Pick if they wish to.
However, I'd love to understand better why you "definitely can't change the behavior". Are there any examples you can provide that would justify that? Is it possible that changing the Pick implementation would break current uses of Pick? If so, how?
I'm just trying to learn more about this issue, and would appreciate any help.
Reacted by Ermolay Romanov, David Hoepelman, Renārs Vilnis, Anna Fidanli and Jeff VanDyke
Conditional types incur additional costs.
Using it when you don't need it is a waste.
Each use adds up.
Also, it's possible for people to want the current behavior when passing in a union type.
I'm on mobile but Pick<{ a:1,b:2 }|{ b:3,c:4 }, "b"> should be { b:2|3 }. I don't see it as too farfetched for there to exist people relying on this behavior
Activity
[-]Mapped Types don't distribute over union types[/-][+]Mapped Types should distribute over union types[/+]eps1lon commentedon Feb 13, 2019
It looks like only
Pickdoes not distribute over union types.Partialis working fine.jack-williams commentedon Feb 14, 2019
The difference seems natural if you look at the definitions of
PartialandPick, to me at least.Specifically the constraint part which is essentially the input to the map.
Partial is a parametric function on objects
T, so applying it uniformly to object types in a union makes sense. Pick is a function on keys; the notion there was ever union of object types has been lost.eps1lon commentedon Feb 14, 2019
Jack Williams (@jack-williams)
That's kind of my issue here: Should I actually have to look at the implementation and then infer different behavior? Or wouldn't it be nicer if they behave the same since they're both classified as
mapped types?Do you have an example where the current behavior of
Pickfor unions is useful?Is this maybe more of an issue with
keyoffor unions? In a perfect world thekeyof { kind: 1; error: string } | { kind: 2; payload: any }would be('kind' | 'error') | ('kind' | 'payload'). Typescript would not flatten this union and keep the information that'kind'is the discriminant. Maybe this touches to much internals which is why I would prefer the standard libPickto use conditionals to distribute over union types.At least the title should be changed from "Mapped types" to "Mapped types that are a function of keys". Otherwise it is misleading that all mapped types do not distribute over unions.
dragomirtitian commentedon Feb 14, 2019
Sebastian "Sebbie" Silbermann (@eps1lon)
Distribution does not happen in all cases for conditional types either. If the type parameter is not used nakedly it will not distribute so you have to know a bit about the type to know if it distributes over unions.
I think we can easily create a distributive version of
Pickwith what the language currently offers. I don't think it is of as general a use as the original version, but it would look something like:eps1lon commentedon Feb 14, 2019
Titian Cernicova-Dragomir (@dragomirtitian) Thanks for code. The union keys trick is new to me.
Do you have a use case were you require
Pickto behave in its current way for union types?dragomirtitian commentedon Feb 14, 2019
Sebastian "Sebbie" Silbermann (@eps1lon) I am not 100% sure that i have used
Pickwith unions. I usually usePickon non-union types.The problem is that I do suspect changing it now would break some code somewhere. Also the distributive version is considerably more complex, it will slow down compilation and I think in most cases
Pickis not used over unions so the people who want it can use custom type such as the one above.Distributive omit below, for completeness :)
eps1lon commentedon Feb 14, 2019
Well many libraries use
Pick(see original Post: react higher-order components in particular). In those casesPickshould be agnostic to the type of the type.Can you show me some examples where you use
Pick? I'm biased towards react and we usePickmostly in HOCs which is why the current behavior is so frustrating.Is there some accepted method to benchmark tsc compilation? I'm not familiar with all the intricacies of the language server. Can I just repeatedly run the cli or do I need to reset some things in between?
jack-williams commentedon Feb 14, 2019
Sebastian "Sebbie" Silbermann (@eps1lon)
Yes, I probably agree that the difference between homomorphic (Partial) and non-homomorphic (Pick) mapped types should be better documented. This does not mean the correct solution is to remove the differences.
Fundamentally I think they are mapping over different things and it doesn't really make sense to force them to behave the same. If you look at the implementation of
PickI'm not even sure how you would implement it as a mapped type that distributes over unions; the object type does not appear in the constraint type ([K in …]).They are useful to me in so far as they work as I expect them to. This isn't a very satisfactory answer, but I've never felt the need for them to distribute.
This will fundamentally break union types because they should be associative. If it doesn't break the correctness of the typechecker, I suspect it will at least cause a non-trivial perf hit.
The crux of the issue, IMO, is that distributing non-homomorphic mapped types over unions is incredibly shaky. They are functions that depend on keys, and the keys change if you view as a union or if you distribute.
As an example, consider the distributive implementation of
Pick.For
JustPayload, distributingpayloadto the branch with kind"C"produces an empty object type{}, and consequently the overall type ofJustPayloadis essentially{}. I'm not sure this is what someone would want or expect.dragomirtitian commentedon Feb 14, 2019
Jack Williams (@jack-williams)
JustPayloadcan be useful with the apropriate type guards (anintype guard should narrow the{}away). But I do agree variations in behavior may be required on what you are trying to achieve.eps1lon commentedon Feb 14, 2019
What do you mean by that? I can still branch depending on the shape if I have a union like
Foo | {}: https://www.typescriptlang.org/play/index.html#src=type%20Foo%20%3D%20%7B%20payload%3A%20string%20%7D%20%7C%20%7B%7D%0D%0A%0D%0Afunction%20getPayload(foo%3A%20Foo)%3A%20string%20%7C%20undefined%20%20%7B%0D%0A%20%20%20%20if%20(%22payload%22%20in%20foo)%20%7B%0D%0A%20%20%20%20%20%20%20%20return%20foo.payload%3B%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20return%20undefined%3B%0D%0A%7Djack-williams commentedon Feb 14, 2019
The
inoperator is terribly unsound. I can do things like:If you are using the
inoperator, why not do that before applyingPickto get rid of the unions?eps1lon commentedon Feb 14, 2019
I just wanted to investigate what you mean by
I still have a union type information.
This is fine by me. After all 3 is assignable to
{}.13 remaining items
jack-williams commentedon Feb 27, 2019
I've slightly lost track of where this proposal is going so I'm recapping, mostly for my own benefit. There seem to be three options:
Pickmapped type innately distribute. I'm not sure this is even possible given how the type is defined.The other two options seem to be oriented around redefining the library definition of
Pick.Pickthat still enforces thekeyofconstraint on the union.Pickthat lets you pick from a key that only exists in some (or none) of the types.Option (3) will systematically produce unions with
{}in; this seems like pathological behaviour to me. Getting any use out of a union with{}relies on avoiding subtype reduction, and depends upon the unsoundinoperator. The reasoning is happening at the level of syntax, rather than the model of types.Options (2) and (3) introduce generic conditional types into the definition. This will further complicate type relations between generic
Picktypes. Before it only relied upon generic mapped types, now it needs to reason at the level of generic conditional types too. The latter are much harder to get right and still have edge cases. I wouldn't be surprised if #27697 meant that a lot ofPick2types people think should be assignable to each other are not.There is also the issue of making users have to juggle the syntax of
T extends unknown ? …to understand what should be a simplePicktype. This feels like quite a high cost to me.eps1lon commentedon Feb 27, 2019
Isn't that already the case for non-union types and Pick currently?
I'm not sure I understand how a
{}type would be problematic at runtime. Could you add some code that would cause trouble at runtime with a distributive Pick that the current Pick would not also cause with non-union types?jack-williams commentedon Feb 27, 2019
For
Omityes; forPickno.My concern is not that much about runtime; it's more the meaning of the types produced. A union of object types with
{}, such as{} | { foo: string } | { bar: number }, really means the same thing as just{}. Sometimes TypeScript will keep the syntax of the type around, and you can narrow usingin, but sometimes TypeScript will reduce the type to its true meaning. For example:In the first
ifthe union type is not reduced, so narrowing byinworks. However, when TypeScript constructs a union type forboth2it will apply subtype reduction that reduces{ a: string } | {}to{}; now narrowing byindoes not work.The difference here, to me, is largely a syntactic artefact.
A
Picktype that by default does not put constraints on the keys is going to be more likely to produce union types with{}. These union types might work as a user expects in some cases, but if union reduction is applied then it will break their code. TypeScript now has to be more careful about applying union reductions that should be semantically fine, but in practice could break a lot of user code.My comments here are largely technical. I think there is a good argument to be data-driven about this and see what happens in the wild. Anecdotally, I've never had a problem with
Pickso I don't find the motivation that strong---but I'm a single data point.zpdDG4gta8XKpMCd commentedon Mar 1, 2019
how can distribution be pre-picked once to work everywhere for one way over another?
RyanCavanaugh commentedon Apr 1, 2019
Rough notes from discussion in #30696:
typescript-bot commentedon Apr 4, 2019
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
davidgomes commentedon Dec 5, 2019
Ryan Cavanaugh (@RyanCavanaugh) I mostly see your point here that the existing
Pickimplementation works for most cases and that users can opt in to the more complex version of Pick if they wish to.However, I'd love to understand better why you "definitely can't change the behavior". Are there any examples you can provide that would justify that? Is it possible that changing the Pick implementation would break current uses of Pick? If so, how?
I'm just trying to learn more about this issue, and would appreciate any help.
AnyhowStep commentedon Jan 3, 2020
Conditional types incur additional costs.
Using it when you don't need it is a waste.
Each use adds up.
Also, it's possible for people to want the current behavior when passing in a union type.
I'm on mobile but
Pick<{ a:1,b:2 }|{ b:3,c:4 }, "b">should be{ b:2|3 }. I don't see it as too farfetched for there to exist people relying on this behaviorsafareli commentedon May 5, 2021
I played with the two version of distributive
Pickwhich. It turns out that one from Titian Cernicova-Dragomir (@dragomirtitian) is the best.This code will outline difference for those which are interested
Playground Link
chbdetta commentedon Jun 8, 2021
I would prefer Ryan Cavanaugh (@RyanCavanaugh) version of
DistributivePickwithStrictUnionfrom #28339 (comment)In this case we won't end up with
{}being in the picked union.