-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Mapped Types should distribute over union types #28339
Comments
It looks like only |
The difference seems natural if you look at the definitions of type Partial<T> = {
[P in keyof T]?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}; Specifically the constraint part which is essentially the input to the map.
Partial is a parametric function on objects |
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 Do you have an example where the current behavior of Is this maybe more of an issue with 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. |
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 type UnionKeys<T> = T extends any ? keyof T : never
type DistributivePick<T, K extends UnionKeys<T>> = T extends any ? Pick<T, Extract<keyof T, K>> : never;
type Union = {
kind: "A",
payload: number
} | {
kind: "B",
payload: string
} | {
kind: "C",
}
type JustKind = DistributivePick<Union, 'kind'> // Pick<{ kind: "A"; payload: number; }, "kind"> | Pick<{ kind: "B"; payload: string; }, "kind"> | Pick<{ kind: "C"; payload: string; }, "kind">
type JustPayload = DistributivePick<Union, 'payload'> // Pick<{ kind: "A"; payload: number; }, "payload"> | Pick<{ kind: "B"; payload: string; }, "payload"> | Pick<{ kind: "C"; }, never> |
@dragomirtitian Thanks for code. The union keys trick is new to me.
Do you have a use case were you require |
@eps1lon I am not 100% sure that i have used 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 Distributive omit below, for completeness :) type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type DistributiveOmit<T, K extends UnionKeys<T>> = T extends any ? Omit<T, Extract<keyof T, K>> : never; |
Well many libraries use
Can you show me some examples where you use
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? |
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
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 type JustPayload = DistributivePick<Union, 'payload'> // Pick<{ kind: "A"; payload: number; }, "payload"> | Pick<{ kind: "B"; payload: string; }, "payload"> | Pick<{ kind: "C"; }, never> For |
|
What do you mean by that? I can still branch depending on the shape if I have a union like |
The const x: JustPayload = 3; If you are using the |
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 |
3 is assignable to The problem is that semantically type Check = {} extends JustPayload ? (JustPayload extends {} ? "They are the same" : "no") : "no"; You are relying on the type-checker to not perform subtype reduction on the union. |
@jack-williams What do you mean by that? The playground tells me it's still All of this sounds more of an issue with I think think we argue with a different premise: You assume that you know the exact shape of a given |
What is the difference between For example, I don't understand why
Is not the same than
And why
Is not the same than
@dragomirtitian Btw, i think you wanted to write |
@VincentLanglet See #28483 (comment) for additional resources. Combined with the comments in this thread it hopefully gives you a decent explanation what is happening. |
@eps1lon Thanks, the link https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#distributive-conditional-types, found in the issue you gave me, helped me a lot |
@VincentLanglet 10x for the catch, fixed. |
Recommend using type Pick2<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
type MappedFooOrBar = Pick2<FooOrBar, keyof FooOrBar>
declare function fn(x: MappedFooOrBar): void;
// Error
fn({ x: true, foo: undefined });
// OK
fn({ x: false }); We're still noodling on whether this is a safe change to make |
@RyanCavanaugh Would it help if I checked the DefinitelyTyped repo with the new version and report back with any breakage? Or is the main concern performance of the type checker? |
@RyanCavanaugh @eps1lon type Foo = { x: false; foo?: string, bar: string }
type Bar = { x: true; foo: string }
type FooOrBar = Foo | Bar
type Pick2<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
type MappedFooOrBar = Pick2<FooOrBar, "x" | "bar"> // error here I fear that since |
@dragomirtitian type UnionKeys<T> = T extends unknown ? keyof T : never;
type Pick2<T, K extends UnionKeys<T>> = T extends unknown ? Pick<T, K> : never;
type Foo = { x: false; foo?: string, bar: string }
type Bar = { x: true; foo: string }
type FooOrBar = Foo | Bar
type MappedFooOrBar = Pick2<FooOrBar, "x" | "bar"> // error here I'm still a little bit confused why you argue against it? Is there something this would break? Are you concerned about perf? Just playing devils advocate? |
@eps1lon A bit of devils advocate (we should consider all sides before making a decision), mostly perf concern. I would restrict
type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
type Foo = { x: false; foo?: string, bar: string }
type Bar = { x: true; foo: string }
type FooOrBar = Foo | Bar
type Pick2<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
type MappedFooOrBar = Pick2<StrictUnion<FooOrBar>, "x" | "bar"> // ok now, but will be type equivalent to
type MappedFooOrBarExpanded = {
x: false;
bar: string;
} | {
x: true;
bar?: undefined;
} But the confusing behavior would still be there initially and it would be the kind of thing you "have to know" about, |
how can distribution be pre-picked once to work everywhere for one way over another? |
Rough notes from discussion in #30696:
|
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
@RyanCavanaugh I mostly see your point here that the existing 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. |
Conditional types incur additional costs. 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 |
I played with the two version of distributive This code will outline difference for those which are interested type Keys<T> = keyof T
type UnionKeys<T> = T extends unknown ? keyof T : never;
// from https://github.com/microsoft/TypeScript/issues/28339#issuecomment-463577347
type DistributivePick<T, K extends UnionKeys<T>> = T extends unknown
? // inlined version of `Pick<T, Extract<keyof T, K>>` (done for nicer type inference)
{ [P in Extract<keyof T, K>]: T[P] }
: never;
// from https://github.com/microsoft/TypeScript/issues/28339#issuecomment-467220238
type DistributivePick2 <T, K extends keyof T> = T extends unknown
? // inlined version of `Pick<T, K>` (done for nicer type inference)
{ [P in K]: T[P] }
: never;
type X =
| {type: "string", foo: string, komara: 1}
| {type: "number", foo: number, kzxzdad: string}
| {type: "undefined", foo: undefined, ajjs:1,asdad:44}
// type UnionKeys_X = "type" | "foo" | "komara" | "kzxzdad" | "ajjs" | "asdad"
type UnionKeys_X = UnionKeys<X>
// type Keys_X = "type" | "foo"
type Keys_X = Keys<X>
// type Z_ = {
// foo: string | number | undefined;
// type: "string" | "number" | "undefined";
// }
type Z_ = Pick<X,"foo"|"type">
// type Z =
// | { type: "string"; foo: string; }
// | { type: "number"; foo: number; }
// | { type: "undefined"; foo: undefined; }
type Z = DistributivePick<X,"foo"|"type">
// type Z2 =
// | { type: "string"; foo: string; }
// | { type: "number"; foo: number; }
// | { type: "undefined"; foo: undefined; }
type Z2 = DistributivePick2<X,"foo"|"type">
type L_ = Pick<X,"foo"|"type"|"kzxzdad">
// ERROR ~~~~~~~~~~~~~~~~~~~~~~
// Type '"type" | "foo" | "kzxzdad"' does not satisfy the constraint '"type" | "foo"'.
// Type '"kzxzdad"' is not assignable to type '"type" | "foo"'.(2344)
// type L =
// | { type: "string"; foo: string; }
// | { type: "number"; foo: number; kzxzdad: string; }
// | { type: "undefined"; foo: undefined; }
type L = DistributivePick<X,"foo"|"type"|"kzxzdad">
type L2 = DistributivePick2<X,"foo" | "type" | "kzxzdad">
// ERROR ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"type" | "foo" | "kzxzdad"' does not satisfy the constraint '"type" | "foo"'.
// Type '"kzxzdad"' is not assignable to type '"type" | "foo"'.(2344)
declare function test(z:Z):void;
declare const x:X
declare const z_:Z_
declare const z:Z
declare const z2:Z2
declare const l:L
test(x)
test(z)
test(l)
test(z2)
test(z_) |
I would prefer @RyanCavanaugh version of In this case we won't end up with |
Search Terms
Mapped Type Union
Suggestion
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:
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
(orOmit
) on their props. This is already a problem withreact-redux
'sconnect
for example.Examples
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: