If you're searching for the specific type, here it is:
type XArgs =
{ foo: any; fooId?: never; bar: any; barId?: never; } |
{ foo: any; fooId?: never; barId: any; bar?: never; } |
{ fooId: any; foo?: never; bar: any; barId?: never; } |
{ fooId: any; foo?: never; barId: any; bar?: never; };
function x({ foo, fooId, bar, barId }: XArgs) { }
x({ foo: "", bar: "" }); // acceptable
x({ fooId: "", bar: "" }); // acceptable
x({ foo: "", fooId: "", bar: "" }); // error
x({ bar: "" }); // error
Hence, XArgs
represents a union with four possible structures. Let's analyze the first one:
{ foo: any; fooId?: never; bar: any; barId?: never }
In this case, both foo
and bar
are required properties of type any</code. However, <code>fooId
and barId
are optional properties denoted by ?
and being values of type never. Since never
lacks any viable value, providing defined fooId
or barId
properties is impossible. Given that optional properties can be omitted, an optional property of type never
becomes essentially restricted. Therefore, in this structure, foo
and bar
must be included while fooId
and barId
should not.
The remaining three union members follow similar patterns but with varying acceptable and prohibited properties. Together, these four union members comprising XArgs
define all conceivable arguments for x()
.
This summarizes the response to your query.
The manual creation of the necessary union may become overly tedious, especially with multiple exclusive unions (requiring exactly one element) or various significant property sets involved.
To mitigate this complexity, the compiler can derive XArgs
through the following calculation:
type AllKeys<T> = T extends unknown ? keyof T : never
type ExclusiveUnion<T, K extends PropertyKey = AllKeys<T>> =
T extends unknown ? (T & { [P in Exclude<K, keyof T>]?: never }) : never;
The AllKeys<T>
type utilizes distributive conditional type principles to calculate the key union of each T
union member. Thus, AllKeys<{a: 0} | {b: 1}>
results in "a" | "b"
.
On the other hand, the ExclusiveUnion<T>
type creates an exclusive version from a union like
{a: string} | {b: number} | {c: boolean}
, explicitly disallowing elements present only in other members. By utilizing
AllKeys
to access keys from other members, the outcome aligns with
{a: string, b?: never, c?: never} | {a?: never, b: number, c?: never} | {a?: never, b?: never, c: boolean}
.
Note that it generates unions through intersections, making it intricate.
Introducing the Expand<T>
recursive conditional type helps consolidate intersections and expand any aliased properties:
type Expand<T> = T extends object ? { [K in keyof T]: Expand<T[K]> } : T;
Subsequently, we formulate XArgs
as an intersection of ExclusiveUnion
s and then apply Expand
for clarity:
type XArgs = Expand<
ExclusiveUnion<{ foo: any } | { fooId: any }> &
ExclusiveUnion<{ bar: any } | { barId: any }>
>;
This translates to
type XArgs =
{ foo: any; fooId?: never; bar: any; barId?: never; } |
{ foo: any; fooId?: never; barId: any; bar?: never; } |
{ fooId: any; foo?: never; bar: any; barId?: never; } |
{ fooId: any; foo?: never; barId: any; bar?: never; };
Try applying this technique to a more complex type for comparison:
type YArgs = Expand<
ExclusiveUnion<{ a: 0 } | { b: 1 } | { c: 2 }> &
ExclusiveUnion<{ x: 9 } | { y: 8 } | { z: 7 }>
>
/* Resultant type YArgs =
{ a: 0, b?: never, c?: never, x: 9, y?: never, z?: never; } |
{ a: 0, b?: never, c?: never, y: 8, x?: never, z?: never; } |
{ a: 0, b?: never, c?: never, z: 7, x?: never, y?: never; } |
{ b: 1, a?: never, c?: never, x: 9, y?: never, z?: never; } |
{ b: 1, a?: never, c?: never, y: 8, x?: never, z?: never; } |
{ b: 1, a?: never, c?: never, z: 7, x?: never, y?: never; } |
{ c: 2, a?: never, b?: never, x: 9, y?: never, z?: never; } |
{ c: 2, a?: never, b?: never, y: 8, x?: never, z?: never; } |
{ c: 2, a?: never, b?: never, z: 7, x?: never, y?: never; } */
Results appear satisfactory!
Review code on Playground