sindresorhus / type-fest

A collection of essential TypeScript types
Creative Commons Zero v1.0 Universal
14.41k stars 548 forks source link

[Proposal] Tagged Union To Intersection #173

Open mohaalak opened 3 years ago

mohaalak commented 3 years ago

Problem

let's say we have these types


type Email = {id: number, type: "Email", email: string}
type Phone = {id: number, type: "Phone", phone: string}

type ContactInfo= Email | Phone

and we want to send it to an API or save it do database but the API accepts something like this

interface ContactInfoToSave {
  id: number;
  type: "Email" | "Phone";
  phone?: string;
  email?: string;
}

UnionToIntersecion doesnot work

if we use UnionToIntersection we get never cause the type cannot be merged to one it's like when you want to intersect UnionToIntersection<string | number> it cannot be done.

Solution

I have a solution for it

 type DistributiveOmit<T, K extends keyof T> = T extends any ? Omit<T, K> : never;

export type TaggedUnionToIntersection<T> = UnionToIntersection<
  Partial<DistributiveOmit<T, keyof T>> & Pick<T, keyof T>
>;

Let me explain how does it work. The default Omit and Pick utility type does not go inside each union type, but it's going to pick only the common type that available in every union type. also keyof works exactly like that only will export the keys is common in each union;

so

type K = keyof ContactInfo; // the only common between Email and Phone is => "type" | "id"

now if we pick these two keys

type Picked = Pick<ContactInfo, keyof ContactInfo>; // {type: "Email" | "Phone", id: number}

Now we want to omit id and type but we want to go inside each union, so we can remove it. for this we can use Distributive conditional types hence the DistributiveOmit.

After that we got something like

type Example = {id: number, type: "Email" | "Phone"} & ({phone?: string} | {email?: string})

now with UnionToIntersection we can merge all of this together and make the ContactInfoToSave

I did not pull request because I thought let's discuss it a little about it. like if the TaggedUnionToIntersection is a good name or if we want to no common between union be Partial or not.

Upvote & Fund

Fund with Polar

papb commented 3 years ago

If I understand the proposal correctly, it seems you want to have a utility type that can take your Email and Phone types as input and produce the ContactInfoToSave as output, is that right? Something like:

type Email = {id: number, type: "Email", email: string}
type Phone = {id: number, type: "Phone", phone: string}

type MyResult = MyType<Email, Phone>;
//=> type MyResult = {
//   id: number;
//   type: "Email" | "Phone";
//   phone?: string;
//   email?: string;
// }

Am I correct? Is this what you want?

If yes, why? Your ContactInfoToSave type is a superset of Email | Phone. It allows some "nonsense" values such as:

{
  id: 1,
  type: 'Email',
  phone: '99999999999'
}

Why not just use Email | Phone?

mohaalak commented 3 years ago

No I have this type

type ContactInfo = Email | Phone

I work with this on my main data but the server need something like

type ContacInfoToSave = {
   type: "Email" | "Phone",
  phone?: string;
  emailAddress?: string
}

and about "nonsense" if you want to go from "ContactInfo" to "ContactInfoToSave" there will not be any nonsense, but for going from "ContactInfoToSave" to "ContactInfo" there will be some cases and it needs validation, I use io ts for validation and decoding.

let's save we want to map ContactInfo to ContactInfoToSave

const map = (c: ContactInfo): ContactInfoToSave => ({
  type: c.type,
  emailAddress: c.email  // there is no c.email
})

there is no c.email or c.phone you should just create some switch case for every type to create a ContactInfoToSave.

papb commented 3 years ago

[...] but the server need something like [...]

Why does the server need that? Why can't the server use Email | Phone?

mohaalak commented 3 years ago

[...] but the server need something like [...]

Why does the server need that? Why can't the server use Email | Phone?

your types should not be dependent of what your server request you should have your types then when you want to connect to server or database you should map it to them.

server or database should not dictate how you express your models and types.

mohaalak commented 3 years ago

[...] but the server need something like [...]

Why does the server need that? Why can't the server use Email | Phone?

you can see that server accept emailAddress but I have email when you want to map you should pattern match on type then return the value but with this type you have all of types that server wants without pattern matching on type field.