Home > Software design >  How to make one object type which will contain properties of all objects of union type, but values w
How to make one object type which will contain properties of all objects of union type, but values w

Time:04-04

How i can make a type which will make from unions of objects a one object type which will contain properties of all objects of union type, but values will intersections.

Example: i need to make from type { foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 } a type {foo: 1 | 2 | 7; bar: 3 | 8}.

Important note: i want to make one object type instead of intersection like {foo: 1 | 2} & {bar: 3}

I have written a type ComplexUnionToIntersection which should do it, but it's ignore properties which is not exist in all objects of union (bar in my examples).

My code:

/**
 * More info: https://fettblog.eu/typescript-union-to-intersection/
 */
export type UnionToIntersection<U> = (
    U extends any ? (k: U) => void : never
) extends (k: infer I) => void
    ? I
    : never;

/**
 * Target type
 */
export type ComplexUnionToIntersection<U> = { o: U } extends { o: infer X }
    ? {
            [K in keyof (X & U)]: (X & U)[K];
      }
    : UnionToIntersection<U>;

Test cases:

// TODO: result of test case must be `{foo: 1 | 2; bar: 3}`
type testCase1 = ComplexUnionToIntersection<{ foo: 1 } | { foo: 2; bar: 3 }>; // actually return `{ foo: 1 | 2; }`

// TODO: result of test case must be `{foo: 1 | 2 | 7; bar: 3 | 8}`
type testCase2 = ComplexUnionToIntersection<
    { foo: 1 } | { foo: 2; bar: 3 } | { foo: 7; bar: 8 }
>;

// TODO: result of test case must be `{foo: 1 | 2; bar: 3 | 8}`
type testCase3 = ComplexUnionToIntersection<
    { foo: 1 } | { foo: 2; bar: 3 } | { bar: 8 }
>;

// TODO: result of test case must be `{foo?: 1 | 2; bar: 3 | 8}`
type testCase4 = ComplexUnionToIntersection<
    { foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }
>;

TS playground here

CodePudding user response:

So the operation is to merge a union of object types into a single object type, where each property key in any of the input union members will appear in the output type, and the value type of that property will be the union of all property value types for that key wherever it appears in the input. And you want a property to be optional in the ouput if it's optional in any of the input union members.

If you just collapse the union into a single object type, you'll have the problem you see where a property will only exist in the output if it exists in every input, but otherwise the property types will be correct. So one approach here would be to first augment each member of the union so it contains all the keys from any union member. So if a property is missing, we should add it, and give it the value type of the never type, which is absorbed in any union.

For example, we start with something like:

{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }

And then augment each member of the union to contain all the keys, something like:

{ foo: 1; bar: never } | { foo?: 2; bar: 3 } | { foo: never; bar: 8 }

And then we merge this new union into a single object, like:

{ foo?: 1 | 2 | never; bar: never | 3 | 8 }

which collapses to

{ foo?: 1 | 2; bar: 3 | 8 }

So let's do it:


type AllKeys<T> = T extends unknown ? keyof T : never

type AddMissingProps<T, K extends PropertyKey = AllKeys<T>> =
    T extends unknown ? (T & Record<Exclude<K, keyof T>, never>) : never;

type MyMerge<T> = { [K in keyof AddMissingProps<T>]: AddMissingProps<T>[K] }

The AllKeys<T> type is a distributive conditional type which gathers every key from every union member:

type TestAllKeys = AllKeys<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
// type TestAllKeys = "foo" | "bar"

The AddMissingProps<T, K> type is also distributive, and for each element of the input union T it adds any keys from K which are not already present in keyof T, and gives them a never type, and note that K defaults to AllKeys<T>:

type TestAddMissingProps = AddMissingProps<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
/* type TestAddMissingProps = 
    ({ foo: 1; } & Record<"bar", never>) | 
    ({ foo?: 2 | undefined; bar: 3; } & Record<never, never>) | 
    ({ bar: 8; } & Record<"foo", never>) */

That is equivalent to the { foo: 1; bar: never } | { foo?: 2; bar: 3 } | { foo: never; bar: 8 } type mentioned above, although it's not written the same way. Since we are still going to process the type it's not important to reduce it here.

Finally, the MyMerge<T> type is an identity mapped type over AddMissingProps<T>. Its only purpose is to iterate over each property and produce a single object type output:

type TestMyMerge = MyMerge<{ foo: 1 } | { foo?: 2; bar: 3 } | { bar: 8 }>
/* type TestMyMerge = {
    foo?: 1 | 2 | undefined;
    bar: 3 | 8;
} */

Looks good!

Playground link to code

  • Related