Schrödinger's Discriminated Union

Erwin Schrödinger owned a fat old cat named Mittens that one of his relatives gave him as a gift. Mittens was relatively low maintenance aside from the occasional vet visit (and that one time that he destroyed the sofa and the wall), but Erwin, being a caring soul, always worried for the cat's health.

Mittens had an old cardboard box. He claimed the box when Erwin moved into the apartment they both currently occupied, and liked to sleep in it and do other things that cats do, like create hairballs. Erwin always worried about Mittens while he was sleeping in his box, unsure if the animal was still alive, and would frequently wake the cat up to check he was ok (a habit that Mittens did not like one bit). Erwin would always relieved that nothing has happened to the cat, even when Mittens rewarded his care by scratching his face. Erwin would also make sure to feed Mittens as an apology for waking him (which Mittens appreciated).

Later in his career, Erwin went on to use this story to explain quantum superposition, granting no credit or rights to Mittens (who's estate are still wrapped up in a legal battle over this to this day), but Mittens' legacy still has a thing or two to teach us.

Discriminated Unions

If we were to type Mittens' behavior, we'd end up with something like this:

enum Status {
  Alive = "alive",
  Dead = "dead",
}

interface LivingCat {
  status: Status.Alive;
  wantsToEat: boolean;
}

interface DeadCat {
  status: Status.Dead;
}

type Mittens = LivingCat | DeadCat;

Or in other words:

  • Mittens may be alive or dead at any moment.
  • Mittens can be hungry, but only if he's alive.

The above pattern is called a Discriminated Union, and we can use this to differentiate objects with a shared property based on which the rest of the properties are determined. The discriminating property must be a literal (e.g. Status.Alive). By using this pattern, TypeScript will know which of the types used in the union the object matches after checking the discriminating property (like Erwin checking if the cat is alive or dead). This is extremely useful to prevent accessing properties that may not exist and for modeling your types according to actual behavior.

A more familiar example is data loading - we have an object in our state that has a LoadingStatus, and has data only if LoadingStatus is LoadingStatus.Success:

enum LoadingStatus {
  Loading = "loading",
  Success = "success",
}

interface LoadedState {
  loadingStatus: LoadingStatus.Success;
  data: MyDataType;
}

interface LoadingState {
  loadingStatus: LoadingStatus.Loading;
}

type State = LoadedState | LoadingState;

Using this typing, TypeScript will prevent us from accessing data before we know the state is done loading. This prevents bugs and types our state closer to how it really behaves in practice.

Remember Mittens and type safely!

Yoav Lavi

Yoav Lavi