Skip to main content

Optional

This is an in-depth explanation of optionals. For simple usage examples, check out the "Getting started" page.

What is it?

An optional is a container of zero or one value. It is an incredibly common primitive123 that enables type-safe management of the possibility that a value is absent or failed to be produced.

Such type-safe management is technically already possible in TypeScript using nullable type unions, which prevent you from accessing a value without checking for null and/or undefined, but lfi introduces an additional concept. In lfi, an optional is simply an iterable that produces zero or one value.

It is represented by the Optional, AsyncOptional, and ConcurOptional types, depending on the async work required to (potentially) produce a value. These types are actually just aliases for Iterable, AsyncIterable, and ConcurIterable, respectively. They don't have different runtime representations from the regular iterable types.

How do I use it?

There are several lfi functions that:

  • Return optionals, such as find, findAsync, and findConcur
  • Accept optionals, such as or, orAsync, and orConcur

For example, consider the following code:

import { find, or, pipe } from 'lfi'

const getAnimal = targetAnimal =>
pipe(
[`dog`, `cat`, `bunny`],
// Return an optional potentially containing the found animal
find(animal => animal === targetAnimal),
// Return the optional's value if it exists, or the result of the callback otherwise
or(() => `no ${targetAnimal}???`),
)

console.log(getAnimal(`dog`))
//=> dog
console.log(getAnimal(`cat`))
//=> cat
console.log(getAnimal(`bunny`))
//=> bunny
console.log(getAnimal(`sloth`))
//=> no sloth???
Playground

Why use iterables to represent optionals?

Optionals returned by lfi functions are just iterables, so they can be lazy and they can be passed to any function that expects a regular iterable, even functions from other libraries. Conversely, any iterable, including one not produced by lfi, can be passed to an lfi function that expects an optional4. This is incredibly convenient for sharing logic, and keeping lfi simple and flexible.

For example, consider the following code:

import { find, map, or, pipe } from 'lfi'

const getAnimal = targetAnimal =>
pipe(
[`dog`, `cat`, `bunny`],
find(animal => animal === targetAnimal),
// This works on the optional because it's just an iterable
map(animal => animal.toUpperCase()),
or(() => `no ${targetAnimal}???`),
)

console.log(getAnimal(`dog`))
console.log(getAnimal(`cat`))
console.log(getAnimal(`bunny`))
console.log(getAnimal(`sloth`))
Playground

We were trivially able to use our existing map logic on the optional value!

Footnotes

  1. Rust's Option enum

  2. Java's Optional class

  3. Haskell's Maybe type

  4. All lfi functions intended for optionals treat iterables containing two or more values as empty optionals.