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, andfindConcur - Accept optionals, such as
or,orAsync, andorConcur
For example, consider the following code:
- Sync
- Async
- Concur
import { find, or, pipe } from 'lfi'
const findWordContaining = string =>
pipe(
[`sloth`, `lazy`, `sleep`],
// Return an optional potentially containing the found word
find(word => word.includes(string)),
// Return the optional's value if it exists, or the result of the callback otherwise
or(() => `no ${string}???`),
)
console.log(findWordContaining(`lot`))
//=> sloth
console.log(findWordContaining(`la`))
//=> lazy
console.log(findWordContaining(`ep`))
//=> sleep
console.log(findWordContaining(`lfi`))
//=> no lfi???
import { asAsync, findAsync, orAsync, pipe } from 'lfi'
const API_URL = `https://api.dictionaryapi.dev/api/v2/entries/en`
const findWordWithPartOfSpeech = partOfSpeech =>
pipe(
asAsync([`sloth`, `lazy`, `sleep`]),
// Return an async optional potentially containing the found word
findAsync(async word => {
const response = await fetch(`${API_URL}/${word}`)
const [{ meanings }] = await response.json()
return meanings.some(meaning => meaning.partOfSpeech === partOfSpeech)
}),
// Return a promise that resolves to the async optional's value if it
// exists, or the result of the callback otherwise
orAsync(() => `no ${partOfSpeech}???`),
)
console.log(await findWordWithPartOfSpeech(`noun`))
//=> sloth
console.log(await findWordWithPartOfSpeech(`verb`))
//=> sloth
console.log(await findWordWithPartOfSpeech(`adjective`))
//=> lazy
console.log(await findWordWithPartOfSpeech(`adverb`))
//=> no adverb???
import { asConcur, findConcur, orConcur, pipe } from 'lfi'
const API_URL = `https://api.dictionaryapi.dev/api/v2/entries/en`
const findWordWithPartOfSpeech = partOfSpeech =>
pipe(
asConcur([`sloth`, `lazy`, `sleep`]),
// Return an concur optional potentially containing the found word
findConcur(async word => {
const response = await fetch(`${API_URL}/${word}`)
const [{ meanings }] = await response.json()
return meanings.some(meaning => meaning.partOfSpeech === partOfSpeech)
}),
// Return a promise that resolves to the concur optional's value if it
// exists, or the result of the callback otherwise
orConcur(() => `no ${partOfSpeech}???`),
)
console.log(await findWordWithPartOfSpeech(`noun`))
// NOTE: This word may change between runs
//=> sloth
console.log(await findWordWithPartOfSpeech(`verb`))
// NOTE: This word may change between runs
//=> sloth
console.log(await findWordWithPartOfSpeech(`adjective`))
//=> lazy
console.log(await findWordWithPartOfSpeech(`adverb`))
//=> no adverb???
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:
- Sync
- Async
- Concur
import { find, map, or, pipe } from 'lfi'
const findWordContaining = string =>
pipe(
[`sloth`, `lazy`, `sleep`],
find(word => word.includes(string)),
// This works on the optional because it's just an iterable
map(word => word.toUpperCase()),
or(() => `no ${string}???`),
)
console.log(findWordContaining(`lot`))
//=> SLOTH
console.log(findWordContaining(`la`))
//=> LAZY
console.log(findWordContaining(`ep`))
//=> SLEEP
console.log(findWordContaining(`lfi`))
//=> no lfi???
We were trivially able to use our existing map logic on the optional value!
import { asAsync, findAsync, mapAsync, orAsync, pipe } from 'lfi'
const API_URL = `https://api.dictionaryapi.dev/api/v2/entries/en`
const findWordWithPartOfSpeech = partOfSpeech =>
pipe(
asAsync([`sloth`, `lazy`, `sleep`]),
findAsync(async word => {
const response = await fetch(`${API_URL}/${word}`)
const [{ meanings }] = await response.json()
return meanings.some(meaning => meaning.partOfSpeech === partOfSpeech)
}),
// This works on the async optional because it's just an async iterable
mapAsync(word => word.toUpperCase()),
orAsync(() => `no ${partOfSpeech}???`),
)
console.log(await findWordWithPartOfSpeech(`noun`))
//=> SLOTH
console.log(await findWordWithPartOfSpeech(`verb`))
//=> SLOTH
console.log(await findWordWithPartOfSpeech(`adjective`))
//=> LAZY
console.log(await findWordWithPartOfSpeech(`adverb`))
//=> no adverb???
We were trivially able to use our existing mapAsync logic on the async
optional value!
import { asConcur, findConcur, mapConcur, orConcur, pipe } from 'lfi'
const API_URL = `https://api.dictionaryapi.dev/api/v2/entries/en`
const findWordWithPartOfSpeech = partOfSpeech =>
pipe(
asConcur([`sloth`, `lazy`, `sleep`]),
findConcur(async word => {
const response = await fetch(`${API_URL}/${word}`)
const [{ meanings }] = await response.json()
return meanings.some(meaning => meaning.partOfSpeech === partOfSpeech)
}),
// This works on the concur optional because it's just an concur iterable
mapConcur(word => word.toUpperCase()),
orConcur(() => `no ${partOfSpeech}???`),
)
console.log(await findWordWithPartOfSpeech(`noun`))
// NOTE: This word may change between runs
//=> SLOTH
console.log(await findWordWithPartOfSpeech(`verb`))
// NOTE: This word may change between runs
//=> SLOTH
console.log(await findWordWithPartOfSpeech(`adjective`))
//=> LAZY
console.log(await findWordWithPartOfSpeech(`adverb`))
//=> no adverb???
We were trivially able to use our existing mapConcur logic on the concur
optional value!
Footnotes
-
All
lfifunctions intended for optionals treat iterables containing two or more values as empty optionals. ↩