Either/Or types in TypeScript

How to enforce that a function expects 'one property or the other' in TypeScript.September 25, 2020

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)

Continue Reading

css architecturedesign systemsfrontend platform

Deep Dive Into the Cascade: Use the @layer Rule to Write More Deterministic CSS

Read Post