What you're requesting is quite similar to an identity operation; if you have a union type like A | B
and access its properties, each property automatically becomes unions. If this is all you need, then you can just use the union as-is, or alternatively, create a mapped type to consolidate the union into a single object type.
The slight complication here is that you want properties appearing in only some of the union members to be optional in the combined type, whereas a union tends to omit such properties. So, the initial step is to convert each union member into a new type with optional properties of type never
for any undefined property in that member. Essentially, we aim to transform {a: 0, b: 1} | {b: 2, c: 3}
into something akin to
{a: 0, b: 1, c?: never} | {a?: never, b: 2, c: 3}
. Subsequently merging these will result in
{a?: 0, b: 1 | 2, c?: 3}
as desired.
The integration appears as follows:
type _Combine<T, K extends PropertyKey = T extends unknown ? keyof T : never> =
T extends unknown ? T & Partial<Record<Exclude<K, keyof T>, never>> : never;
type Combine<T> = { [K in keyof _Combine<T>]: _Combine<T>[K] }
In this scenario, _Combine<T>
serves as a utility type utilizing distributive conditional types to divide T
into union members and execute operations on them. The primary purpose of the generic parameter default value for K
is to compile all keys from the members of T
(
T extends unknown ? keyof T : never
). Simultaneously, we intersect every member of
T
with an object incorporating optional keys of type
never
for each key in
K</code not facilitated by that specific member of <code>T
.
Conversely, Combine<T>
represents an identity mapped type over _Combine<T>
, streamlining complex instances like
({a: 0, b: 1) & Partial<Record<"c", never>>) | ({b: 2, c: 3} & Partial<Record<"a", never>>)
toward the anticipated
{a?: 0, b: 1 | 2, c?: 3}
.
Let's test it out with your sample:
type O = {
type: "bar";
foo: string;
bar: number;
} | {
type: "baz";
foo: string;
baz: boolean;
}
type Z = Combine<O>;
/* type Z = {
type: "bar" | "baz";
foo: string;
bar?: number | undefined;
baz?: boolean | undefined;
} */
Everything seems satisfactory. The essential type
and foo
properties are mandatory since they appear in all members of O
, while the additional bar
and baz
properties are elective, given their absence from at least one O
member.
Playground link for code