DEV Community

Cover image for A Guide to Using the Option Type in TypeScript
Martin Persson
Martin Persson

Posted on

A Guide to Using the Option Type in TypeScript

Table of Contents

Introduction

In TypeScript and other statically typed languages, handling variables that may or may not contain values can be tricky and error-prone. Developers often encounter challenges with null and undefined, which can lead to unexpected behaviors and runtime errors.

The Option type emerges as an elegant solution to this problem. Inspired by functional programming paradigms, the Option type is a powerful construct that encapsulates the idea of an optional value. Unlike null or undefined, it provides a more robust, expressive, and type-safe way to represent values that might be absent.

The Problem with Null and Undefined

The use of null and undefined to represent the absence of value has been a common practice in programming. However, it poses challenges:

  • Type Safety: TypeScript’s type system considers null and undefined as valid values for any type, potentially leading to type errors.
  • Error-Prone: Failure to check for null or undefined can result in runtime errors.
  • Lack of Expressiveness: It does not clearly convey the programmer's intent, making the code harder to understand and maintain.

The Option Type: A Better Approach

The Option type addresses these challenges by introducing a clear and structured way to handle optional values. Instead of using null or undefined, you use a special type that represents two distinct states:

  • Some: A value is present, and it's wrapped in this variant.
  • None: There is no value, represented explicitly by this variant.

This pattern ensures that you must handle both cases explicitly, thereby enhancing type safety and code clarity. By forcing developers to consider the possibility of a missing value, the Option type encourages a more thoughtful and robust approach to programming.

The concept of the Option type is a fundamental pattern in functional programming languages like Scala and Haskell and has been adapted in many modern programming libraries and languages, including TypeScript.

In the following sections, we will explore how to define and use the Option type in TypeScript, see why it's a preferable alternative to using null or undefined, and learn how to utilize it with the help of the FP-TS library to create more expressive, maintainable, and error-resistant code.

Whether you are a seasoned developer or new to functional programming concepts, understanding the Option type will empower you to write cleaner and more robust TypeScript code. Let's dive into the details and explore how this powerful construct can elevate your coding practices!

What is the Option Type?

Here's a simple definition of the Option type in TypeScript:

type Some<T> = {
  _tag: "Some",
  value: T
}

type None = {
  _tag: "None"
}

type Option<T> = Some<T> | None
Enter fullscreen mode Exit fullscreen mode

The Option type's beauty lies in its simplicity and the robustness it brings to your code. Let's break down the structure and understand what's happening:

  • Some: Represents the presence of a value. The _tag: "Some" allows us to discriminate this type, and the generic type T allows us to encapsulate any type of value within.
  • None: Represents the absence of a value. The _tag: "None" helps us distinguish this case.

The type Option<T> is a union of Some<T> and None. When you have a variable of type Option<T>, TypeScript knows it could be one of these two variants. This promotes a conscious and explicit handling of the "absence" case, something that using null or undefined doesn't enforce.

Why Not Just Use Null or Undefined?

Using null or undefined directly may lead to unexpected errors if not handled correctly. With Option, you're guided by the type system to address both the presence and absence of a value, leading to safer code.

Let's see an example with a divide function, first without using the Option type:

const divide = (x: number): number => {
  return 2 / x;
};
Enter fullscreen mode Exit fullscreen mode

This will let TypeScript know that we pass in a number and return a number. But what if we pass in 0? Then we will get Infinity back. We can fix this like so:

const divide = (x: number): number => {
  if (x === 0) {
     throw new Error("Can't divide by zero");
  }
  return 2 / x;
};
Enter fullscreen mode Exit fullscreen mode

But this approach introduces a problem: we now have a potential runtime exception that must be handled whenever this function is called. This complicates the usage of the function, and the requirement to handle this exception might not be clear to the developer using the function.

Let's use an Optionfor this instead:

const some = <T>(value: T): Option<T> => ({
  _tag: "Some",
  value,
});

const none: Option<never> = {
  _tag: "None",
};

const divide = (x: number): Option<number> => (x === 0 ? none : some(2 / x));

const a = divide(4); // {_tag: "Some", value: 0.5}

const b = divide(0); // {_tag: "None"}
Enter fullscreen mode Exit fullscreen mode

In this example, we have defined a divide function that returns an Option<number>. If the input is 0, the function returns none, representing the absence of a value (since division by zero is Infinity). Otherwise, it returns some(2 / x), representing the presence of the result.

By using the Option type, we ensure that the possibility of division by zero is handled explicitly in the type system, enhancing code safety and clarity. This approach forces the caller to handle both cases (Some and None) explicitly, providing a clear contract that aids in understanding and maintaining the code.

Here's how you might use this divide function in practice:

const result = divide(input);
if (result._tag === "None") {
  console.log("Division by zero!");
} else {
  console.log(`Result is: ${result.value}`); // Result is: 0.5
}
Enter fullscreen mode Exit fullscreen mode

By pattern-matching on the _tag field, we can determine whether the result is Some or None and handle both cases accordingly. This approach makes our code more robust and communicates our intentions clearly to other developers who might read or maintain it.

Using Options in FP-TS

If you want to leverage the power of the Option type in your TypeScript projects without defining the type and associated functions yourself, you can use the FP-TS library. FP-TS provides a built-in implementation of the Option type along with various utility functions to make working with Options more expressive and concise.

Installation

To get started, you'll need to install the FP-TS library:

npm install fp-ts
Enter fullscreen mode Exit fullscreen mode

Basic Usage

Import the Option type and related functions from the FP-TS library:

import { Option, some, none, fold } from 'fp-ts/Option';
Enter fullscreen mode Exit fullscreen mode

You can now use some and none to create Option values and utilize other functions provided by FP-TS to work with them.

Example of using FP-TS Option

Define Functions
We'll start by defining two simple functions to double a number and subtract one from it.

Create a Pipeline
We'll use pipe to create a pipeline of operations to process our input. In functional programming, pipe is a way to combine functions in a sequential manner. It takes the output of one function and uses it as the input for the next. This allows us to create a sequence of transformations in a very readable and maintainable way.

Use fold
Finally, we'll use fold to extract the final result from our Option. The fold function is a way to handle both cases of an Option (Some and None), giving us the ability to decide what to do in each scenario. This provides a neat way to finalize our computation and produce a concrete result.

Here's the code example:

import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';

const double = (n: number): number => n * 2;
const subtractOne = (n: number): number => n - 1;

// Function to transform the number
const transformNumber = (input: number): string => pipe(
  input,
  O.fromNullable,          // Transform into Option
  O.map(double),           // Double the number
  O.map(subtractOne),      // Subtract one
  O.fold(
    () => 'No value provided or invalid input',
    result => `Result: ${result}`
  )
);

console.log(transformNumber(5)); // Output: Result: 9
console.log(transformNumber(null)); // Output: No value provided or invalid input
Enter fullscreen mode Exit fullscreen mode

In this example:

  • We use O.fromNullable to transform our input into an Option. If the input is null or undefined, this results in a None. Otherwise, it results in a Some containing the value.
  • The O.map functions apply our transformation functions (double and subtractOne) within the Option context.
  • Finally, O.fold allows us to handle the two possible cases (Some and None) and produce a final string result.

This example demonstrates how to create a pipeline of operations using FP-TS's Option, handling potential absence of value in a clear and type-safe manner. It shows the power of functional programming concepts and how they can make your code more robust and expressive.

Conclusion

The Option type and the FP-TS library offer robust solutions for handling optional values in a type-safe way. By forcing developers to handle both the presence and absence of values explicitly, they lead to more maintainable and error-free code. Whether you define the Option type yourself or use FP-TS, embracing these functional programming concepts can enhance your TypeScript development experience.

Additional Resources

For those interested in diving deeper into the concepts and practices surrounding the Option type and functional programming in TypeScript, here are some valuable resources:

Top comments (2)

Collapse
 
htho profile image
Hauke T.

Hi,

thank you for your post.

I considered using an Option type (and also encapsulating the error in the None type). But then I realized this is not good for public APIs. Developers are not used to the Option type and it is not a language standard (as compared to Rust). Hence they will have a hard time using it.

I also dislike the runtime-overhead: each time a value is returned, a new object is instantiated.

A Suggestion

In this approach the _tag is only a type thing and does not need to be in the runtime object.

const some = <T>(value: T): Option<T> => ({value} as Some<T>);
const none: Option<never> = {} as None;

const divide = (x: number): Option<number> => (x === 0 ? none : some(2 / x));

const a = divide(4); // typescript thinks: {_tag: "Some", value: 0.5} - but actually {value: 0.5}
const b = divide(0); // typescript thinks: {_tag: "None"} - but actually {}
Enter fullscreen mode Exit fullscreen mode

and then you check like this:

const result = divide(input);
if ("value" in result) { // this is how its intended by TypeScript devs
  console.log("Division by zero!");
} else {
  console.log(`Result is: ${result.value}`); // Result is: 0.5
}
Enter fullscreen mode Exit fullscreen mode

I Still Prefer undefined

But to be completely honest: I don't see the advantage.
As long as everyone is fine with using undefined everywhere, this is still the best solution IMO:

const divide = (x: number): number | undefined => (x === 0 ? undefined : 2 / x);

const result = divide(input);
if (result === undefined) {
  console.log("Division by zero!");
} else {
  console.log(`Result is: ${result.value}`); // Result is: 0.5
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
martinpersson profile image
Martin Persson

Hi

Thank you for your feedback!

I completely understand where you're coming from. The majority of TypeScript developers, being more familiar in OOP than FP, might find the Option type a bit foreign since it's not a native aspect of the language.

I've used FP-TS it in some projects, but as you pointed out, and from my experience, not all team members are as enthusiastic about it as I am...

I'm trying to introduce more FP patterns to our team because I think there is benefits to it.

Your suggestion on minimizing runtime overhead by keeping the _tag only at the type level is quite clever. And yes, for many, sticking to the familiar territory of undefined is probably the more straightforward and preferred approach.

Thanks again for your insights!