Either/Or types in TypeScript
How to enforce that a function expects 'one property or the other' in TypeScript.September 25, 2020TypeScript 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)
Continue Reading
I'm joining Honeycomb.io
I'm excited to share that after 3.5 years at Amplitude I'm joining Honeycomb in a little over a week.
2021: Year in Review
A review of 2021.