Options are a powerful functional programming construct. They are commonly one of the first constructs mentioned when learning functional programming.
One nice way to think of them is as the programmer’s version of Schrodinger’s cat. There’s a box, and inside the box, there may be a value. Or maybe there isn’t. In either case, it’s impossible to know until the box is opened.
To bring the Option object to JavaScript and TypeScript, I wrote and published a fully typed, zero-dependency implementation of the functional programming Option object to NPM called excoptional
.
The README also reviews Options and their capabilities and contains more information on various methods and capabilities. Take a look if you’re interested in learning more after reading this piece.
More formally, Options are an abstraction that works well with functions that may or may not return a value. Options often obviate the need for null
and undefined
checks and provide mechanisms to safely and declaratively transform the value they may (or may not) contain.
Options take care of the repetitive null
and undefined
checks that might otherwise be needed when working with values returned by functions that may or may not have a value.
Here’s a quick example with two versions of uppercasing function. One takes a string
and returns either a string
or undefined
.
The other takes an Option<string>
and returns an Option<string>
. Note the uniformity of the return type compared to string | undefiend
in the first example.
const { Some, None } = require('excoptional');
// Without Options
const uppercaseStr = (value) => {
if (value !== undefined) {
return value.toUpperCase();
}
return undefined;
}
const valOne = uppercaseStr("hello world"); // => string
const valTwo = uppercaseStr(undefined); // => undefined
// With Options
const valThree = Some("hello world") // An option containing "hello world"
.map(str => str.toUpperCase()); // => Option<string>
const valFour = None() // An option with no underlying value
.map(str => str.toUpperCase()); // => Option<string>
In the first example, the argument is checked to make sure it’s not undefined
.
TypeScript’s strict mode helps in these circumstances, but only in validating that when calling the function, a string
value is always passed in (instead of undefined
). The function can still return undefined
and callers of uppercaseStrOne
will have to check the return value is again not undefined
before passing the result to the next function (or implement these undefined checks in every function).
In the second example, we call the map
method on the Option passing in a function that transforms the underlying value (by uppercasing it). No undefined
check is needed in the function passed to map
.
This works regardless if the Option contains an underlying value or not like with valFour
The result remains an Option<string>
allowing for it to be continuously chained with additional map
calls that further transform the underlying value. No more undefined
or null
checks needed. Since the type remains an Option<string>
, the context that there may not be a value here is maintained.
Options provide the ability to remove errors (like the one below) by wrapping values that may be undefined in an Option
.
Uncaught TypeError: Cannot read property 'foo' of undefined at myIncrediblyImportantFunction (index.js:8)
Using Options necessitates constructing instances. The example above uses excoptional
which expose functions Some
and None
which, when invoke return Options.
Some()
takes one argument and is used to create an instance of an Option with that argument as the underlying value. None()
is used when there is no underlying value and so takes no arguments.
Depending on the library, creating instances may be slightly different but they all have similar semantics.
Working with functions that return Options instead of direct values (along with null
and undefined
) leads to a new set of semantics and usage patterns, but Options provide solutions for all scenarios.
Let’s add a validation function before upper casing to check the string length is sufficiently long.
From the example above, the map
method worked for a transformation function that returned a value that is not an Option
. What about transformation functions that themself return an Option? Here’s one.
const { Some, None } = require('excoptional');
const getIfValid = (val) => {
if (val.length > 2) {
return Some(val);
} else {
return None();
}
};
Some("hi")
.flatMap(getIfValid)
.log(); // Log the result
If the provided transformation function returns an Option
, we must use flatMap
. If map
were to be used, the result would be a nested Option. To see this, copy the above snippet into runkit and change flatMap
to map
. Try also shortening the string to 2 characters or fewer.
You can also npm install
the module (excoptional
) locally to run examples and test out its various methods.
The TLDR here is:
map
when the function does not return an Option
flatMap
when the function does return an Option
Look out for the And Then… section for an even better solution
Eventually, at some point, the underlying value is needed. The goal though is to avoid extracting it for as long as possible and instead pass around the Option
instance. In doing so, the context that there may or may not be a value is maintained, and the ability to define a fallback value (if it’s a None
) is left to the final caller.
This is to avoid retrieving the underlying value from the Option, working with it, putting it into an Option later on, retrieving it again, putting it back into an Option again etc. Doing this is an anti-pattern and the methods (map
, flatMap
, then
and others) should be used instead.
At some point though, you will need the value. Options of course provide a mechanism to retrieve the underlying value or a specified alternative if there is no underlying value through the getOrElse
method.
A quick example:
const { Some, None } = require('excoptional');
// Returns Some("jackpot") or None();
const getAnOption = () => Math.random() > .5 ?
Some("jackpot") :
None();
// myOpt is either Some("jackpot") | None()
const myOpt = getAnOption();
// get the undelrying value - "jackpot" - or default
// to the provided string - no jackpot - if it's a
// None.
const value = myOpt.getOrElse("no jackpot");
When you need the underlying value, always use getOrElse
and provide an alternative value in case the Option
is a None
.
This implementation of Option
also exposes a then
method which has similar behavior to Promise.then
and removes having to ask and answer the question, to map
or to flatMap
as reviewed above.
Regardless of whether the function used to transform the underlying value returns an Option
or not, the then
method will work as expected and avoid creating a nested Option.
A quick example:
const { Some, None } = require('excoptional');
// Returns a string
const appendWorld = (str) => str + " world";
// Returns an Option<string>
const maybeAppendExclamationPoint = (str) => {
return Math.random() > .5 ?
Some(str + "!") :
None();
};
// `then` works regardless of the type that the function passed to it returns
const myOpt = Some("hello")
.then(appendWorld)
.then(maybeAppendExclamationPoint)
.log();
For either function, using then
works correctly and won’t result in a nested Option
.
then
will always return an Option.
Promise.then
always returns a promise for the same reason
This library exposes several additional helper methods to provide more capability and an exceptional development experience.
Above there was a call to the log
method which conveniently console logs the Option (simpler than doing console.log(myOption)
) - and nicely formats the instance.
Similarly, there’s a logAndContinue
method which logs the instance and returns it; extremely useful when there is a chain of method calls and you want to inspect the value at any point in that chain without breaking it apart just for debugging.
Several other methods exist that are common to several Option libraries and Options in other languages. These include isSome
, isNone
, filter
, etc.
Take a look at the GitHub repo or NPM page for a complete listing of the available methods. Many of these methods have extensive documentation and examples demonstrating how they can be used.
Options make it easy to continually and safely manipulate a value (even when it doesn't exist) in a declarative and transparent way while preserving the context that the value may (or may not) exist throughout your code.
This library, excoptional
, provides the best JavaScript and TypeScript support possible along with detailed documentation and examples.
It is a standalone package with 0 dependencies and 100% test coverage.
I hope you find it (and more broadly, Options) useful.