Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Proposal: A version of Pick that distributes over union types #132

Closed
novemberborn opened this issue Oct 5, 2020 · 3 comments · Fixed by #841
Closed

Proposal: A version of Pick that distributes over union types #132

novemberborn opened this issue Oct 5, 2020 · 3 comments · Fixed by #841

Comments

@novemberborn
Copy link

novemberborn commented Oct 5, 2020

See microsoft/TypeScript#28791 (comment):

The issue is that Pick doesn't distribute over union types, and therefore your Omit type doesn't either. Here's a simplified version of your example:

type A = { name?: string, variant: 'a' };
type B = { name?: string, variant: 'b' };
type C = A | B;
type X = Omit<C, 'name'>;  // { variant: 'a' | 'b' }

Because Pick isn't distributive over union types, when the input type is a union it is effectively treated as a single object type with only the common properties, each having a union of the respective property types. So, Pick sees C as equivalent to { name?: string, variant: 'a' | 'b' } and X therefore is { variant: 'a' | 'b' }, which isn't assignable back to C.

There are two ways you can fix the issue. The simplest is to declare C as a single object type (and this would be my recommendation):

type C = { name?: string, variant: 'a' | 'b' };

The other is to make Omit be distributive over union types:

export type AllKeys<T> = T extends T ? keyof T : never;
export type Omit<T, K extends AllKeys<T>> = T extends T ? Pick<T, Exclude<keyof T, K>> : never;

This may look a bit odd, but the T extends T ? XXX : never pattern simply means "when T is a union type, distribute XXX over each constituent of T and union together the results".

When you use the distributive form of Omit you now get

type A = { name?: string, variant: 'a' };
type B = { name?: string, variant: 'b' };
type C = A | B;
type X = Omit<C, 'name'>;  // { variant: 'a' } | { variant: 'b' }

and X now is assignable back to C.

The solution I came up with is:

type AllKeys<T> = T extends T ? keyof T : never;
type Select<T, K extends AllKeys<T>> = T extends T
  ? Pick<T, Exclude<keyof T, Exclude<keyof T, K>>>
  : never;

Note that here I want a Pick that is distributive, and the above was about Omit. I suppose we'd want both?

Not sure what to name this though.

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • The funding will be given to active contributors.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@enricozb
Copy link

enricozb commented Oct 13, 2022

I would call these DistributedOmit and DistributedPick. Also both of these can be implemented using the built-in Omit and Pick like this:

type AllKeys<T> = T extends T ? keyof T : never;
type DistributedOmit<T, K extends AllKeys<T>> = T extends T ? Omit<T, K> : never;
type DistributedPick<T, K extends AllKeys<T>> = T extends T ? Pick<T, K> : never;

This was referenced Oct 15, 2023
@sindresorhus
Copy link
Owner

If anyone wants to work on this, see the initial attempt in #710

Copy link
Owner

Thank you @henriqueinonhe for contributing to close this issue! ⭐

The rewards from this issue, totalling $100, has been shared with you.

What now?

  1. Create a Polar account
  2. See incoming rewards & setup Stripe to receive them
  3. Get payouts as backers finalize their payments

If you already have a Polar account setup, you don't need to do anything.

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants