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 | B means either A, 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 never properties where it isn't.

Code Samples

interface ThingOne {
  shirtColor: string;
}
interface ThingTwo {
  hairColor: string;
}
type Thing = ThingOne | ThingTwo;

๐Ÿ’ป playground


const bothThings = {
  shirtColor: 'red',
  hairColor: 'blue',
};
const thing1: ThingOne = bothThings;  // ok
const thing2: ThingTwo = bothThings;  // ok

๐Ÿ’ป playground


interface OnlyThingOne {
  shirtColor: string;
  hairColor?: never;
}
interface OnlyThingTwo {
  hairColor: string;
  shirtColor?: never;
}
type ExclusiveThing = OnlyThingOne | OnlyThingTwo;

๐Ÿ’ป playground


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',
};

๐Ÿ’ป playground


interface Vector2D {
  x: number;
  y: number;
  z?: never;
}

๐Ÿ’ป playground


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.

๐Ÿ’ป playground


interface ThingOneTag {
  type: 'one';
  shirtColor: string;
}
interface ThingTwoTag {
  type: 'two';
  hairColor: string;
}
type Thing = ThingOneTag | ThingTwoTag;

๐Ÿ’ป playground


type XOR<T1, T2> =
    (T1 & {[k in Exclude<keyof T2, keyof T1>]?: never}) |
    (T2 & {[k in Exclude<keyof T1, keyof T2>]?: never});

๐Ÿ’ป playground


type ExclusiveThing = XOR<ThingOne, ThingTwo>;
const allThings: ExclusiveThing = {
  //  ~~~~~~~~~ Types of property 'hairColor' are incompatible.
  shirtColor: 'red',
  hairColor: 'blue',
};

๐Ÿ’ป playground