Either/Or types in TypeScript

September 25, 2020

typescript

TypeScript lets us communicate to other users of a function what can be passed to it. Sometimes, it’s straightforward to apply types to the parameter of a function.

const add = (num1: number, num2: number) => {
  return num1 + num2;
};

Other times, it can be a bit more difficult. One of those cases is when you have what I like to call an “either, or” type. This is when you want to express that users can only pass an object that contains either “this key”, or “that key”. If a user passes a parameter with both keys, unexpected behavior could happen.

We want to write a type that expresses the following rules:

// Properties can include `one`, or `theOther`. But it cannot include both:

const properties = { one: "hi", theOther: "bye" }; // Invalid
const properties = { one: "hi" }; // Valid
const properties = { theOther: "bye" }; // Valid

When we don’t enforce the “either/or” type

We’ll start by writing out an example that doesn’t prevent consumers from passing a parameter that includes both one and theOther.

In this case, if we don’t enforce that consumers should only pass a parameter with either one or theOther we run into a less-than-ideal scenario. If both one and theOther are passed, one will always be returned, and theOther will never be returned.

interface OneOrTheOther {
  one?: string;
  theOther?: string;
}

const oneOrTheOther = ({ one, theOther }: OneOrTheOther) => {
  if (one) {
    return one;
  } else {
    return theOther;
  }
};

In our initial example, if we try to call oneOrTheOther with a parameter that includes one and theOther, we don’t get an error:

oneOrTheOther({ one: "hi", theOther: "bye" }); // No TypeScript error, `one` is returned

When we enforce the “either/or” type

We can use a combination of TypeScript’s never and union types to enforce that a parameter can either include one, or theOther, and never both.

// We use the `never` type to express that you may NEVER pass `theOther` if you have passed `one`.
interface OnlyOne {
  one: string;
  theOther?: never;
}

// We use the `never` type to express that you may NEVER pass `one` if you have passed `theOther`.
interface OnlyTheOther {
  one?: never;
  theOther: string;
}

// You can only pass a parameter that meets the requirements of `OnlyOne`
// OR
// you can only pass a parameter that meets the requirements of `OnlyTheOther`
type OneOrTheOther = OnlyOne | OnlyTheOther;

const oneOrTheOther = ({ one, theOther }: OneOrTheOther) => {
  if (one) {
    return one;
  } else {
    return theOther;
  }
};

Let’s take a look at what this looks like when someone tries to pass a parameter with both one and theOther to the oneOrTheOther function! If we call oneOrTheOther with an invalid parameter:

oneOrTheOther({ one: "hi", theOther: "bye" });

we get the following error:

Argument of type '{ one: string; theOther: string; }' is not assignable to parameter of type 'OneOrTheOther'.
  Type '{ one: string; theOther: string; }' is not assignable to type 'OnlyTheOther'.
    Types of property 'one' are incompatible.
      Type 'string' is not assignable to type 'undefined'.ts(2345)

Get the free design systems newsletter

Whether you're new to design systems or a seasoned pro, this newsletter is for you.

Join over 70 developers learning about design systems now:

All content © Mae Capozzi