The issue at hand
When dealing with unions, it's important to adhere to certain guidelines:
- Only access shared properties: Remember that you can only access properties that are common to all members of the union. For instance, using
if (x.a)
will throw an error if a
doesn't exist on every member.
- Excess properties are permitted: TypeScript follows structural typing, allowing object types to have additional properties. This means that a type like
{ foo: string, bar: number }
can be assigned to a type like { foo: string }
. Use user-defined type guards instead of inline checks to differentiate unions effectively.
- To differentiate unions, favor user-defined type guards over inline checks: Type guards signal TypeScript to narrow down the type being checked. In your case, using
if (typeof x.a !== "undefined")
won't work as expected.
The resolution
Ensure your union is mutually exclusive. Inform TypeScript that either a
or b
can exist, but never both simultaneously.
declare const x: { a?: object, b?: undefined } | { b: number, a?: undefined }
Note that unwanted properties should be marked as optional. If these properties are made required, x
must explicitly have them set to undefined
.
Now, you can utilize these techniques to manipulate x
.
function isDefined<T>(candidate: T | null | undefined): candidate is T {
return candidate != null;
}
if (x.a) {
const Q = x.a; // object
}
if (isDefined(x.a)) {
const Q = x.a; // object
}
if (typeof x.a !== "undefined") {
const Q = x.a; // object
}
Though, the method involving the in
operator remains ineffective. This prevents false positives by ensuring that unwanted properties can only exist if explicitly set to
undefined</code.</p>
<pre><code>function test(x: { a?: object, b?: undefined } | { b: number, a?: undefined }): void {
if ('a' in x) {
x.a; // object | undefined (correct). We cannot assume object here.
}
}
test({ b: 1, a: undefined }); // "a" is not an object!
Tip: leverage the ExclusiveUnion
helper function
Instead of manually marking unwanted properties as ?undefined
, streamline the process with a helper function.
declare const x: ExclusiveUnion<{ a?: object } | { b: number }>;
Implementation:
type DistributedKeyOf<T> =
T extends any
? keyof T
: never;
type CreateExclusiveUnion<T, U = T> =
T extends any
? T & Partial<Record<Exclude<DistributedKeyOf<U>, keyof T>, never>>
: never;
type ExclusiveUnion<T> = CreateExclusiveUnion<T>;