Item 63: Use Optional Never Properties to Model Exclusive Or
May 10, 2024 ยท View on GitHub
Things to Remember
- In TypeScript, "or" is "inclusive or":
A | Bmeans eitherA,B, or both. - Consider the "both" possibility in your code, and either handle it or disallow it.
- Use tagged unions to model exclusive or where it's convenient. Consider using optional
neverproperties where it isn't.
Code Samples
interface ThingOne {
shirtColor: string;
}
interface ThingTwo {
hairColor: string;
}
type Thing = ThingOne | ThingTwo;
const bothThings = {
shirtColor: 'red',
hairColor: 'blue',
};
const thing1: ThingOne = bothThings; // ok
const thing2: ThingTwo = bothThings; // ok
interface OnlyThingOne {
shirtColor: string;
hairColor?: never;
}
interface OnlyThingTwo {
hairColor: string;
shirtColor?: never;
}
type ExclusiveThing = OnlyThingOne | OnlyThingTwo;
const thing1: OnlyThingOne = bothThings;
// ~~~~~~ Types of property 'hairColor' are incompatible.
const thing2: OnlyThingTwo = bothThings;
// ~~~~~~ Types of property 'shirtColor' are incompatible.
const allThings: ExclusiveThing = {
// ~~~~~~~~~ Types of property 'hairColor' are incompatible.
shirtColor: 'red',
hairColor: 'blue',
};
interface Vector2D {
x: number;
y: number;
z?: never;
}
function norm(v: Vector2D) {
return Math.sqrt(v.x ** 2 + v.y ** 2);
}
const v = {x: 3, y: 4, z: 5};
const d = norm(v);
// ~ Types of property 'z' are incompatible.
interface ThingOneTag {
type: 'one';
shirtColor: string;
}
interface ThingTwoTag {
type: 'two';
hairColor: string;
}
type Thing = ThingOneTag | ThingTwoTag;
type XOR<T1, T2> =
(T1 & {[k in Exclude<keyof T2, keyof T1>]?: never}) |
(T2 & {[k in Exclude<keyof T1, keyof T2>]?: never});
type ExclusiveThing = XOR<ThingOne, ThingTwo>;
const allThings: ExclusiveThing = {
// ~~~~~~~~~ Types of property 'hairColor' are incompatible.
shirtColor: 'red',
hairColor: 'blue',
};