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
lfi
functions intended for optionals treat iterables containing two or more values as empty optionals. ↩