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)

Let's talk coding.

I send articles about working as a software engineer, tips and tricks about React and building component libraries, and the occasional personal post. You can unsubscribe at any time.

All content © Mae Capozzi