@othree.io/optional
v3.0.0
Published
Implementation of Optional values
Downloads
520
Readme
@othree.io/optional
A TypeScript implementation of the Optional/Maybe monad for handling nullable values and errors without explicit null checks.
Install
npm install @othree.io/optionalQuick start
import { Optional, Empty, Try, TryAsync } from '@othree.io/optional'
const name = Optional('Alice')
name.get() // 'Alice'
name.isPresent // true
const nothing = Empty<string>()
nothing.isEmpty // true
nothing.orElse('default') // 'default'Creating Optionals
Wrapping a value
const maybeUser = Optional(user) // present if user is not null/undefined
const empty = Optional(undefined) // empty
const empty = Optional(null) // emptyEmpty with an error
const failed = Optional(undefined, new Error('Not found'))
failed.isEmpty // true
failed.getError() // Error('Not found')Empty shorthand
const empty = Empty<string>() // empty, no error
const failed = Empty<string>(new Error('Not found')) // empty, with errorWrapping code that might throw
const result = Try(() => JSON.parse(raw))
// present with parsed value, or empty with the caught error
const result = await TryAsync(() => fetch('/api/user').then(r => r.json()))
// same, but for async operationsExtracting values
get
Returns the value or throws. If the Optional carries an error, that error is thrown. Otherwise an EmptyOptionalError is thrown.
Optional('hello').get() // 'hello'
Empty().get() // throws EmptyOptionalError
Empty(new Error('!')).get() // throws Error('!')orElse
Returns the value or the provided fallback. If the Optional is in an error state, the error is thrown instead of returning the fallback.
Optional('hello').orElse('world') // 'hello'
Empty().orElse('world') // 'world'orThrow
Returns the value or throws the provided error. If the Optional already carries an error, that original error is thrown.
Optional('hello').orThrow(new Error('!')) // 'hello'
Empty().orThrow(new Error('!')) // throws Error('!')Transforming values
map / mapAsync
Transforms the value if present. If the callback returns an Optional, it is automatically flattened.
Optional({ name: 'Alice' })
.map(user => user.name)
.get() // 'Alice'
Optional({ name: 'Alice' })
.map(user => Optional(user.name))
.get() // 'Alice' (flattened)
// Empty Optionals pass through untouched
Empty<string>()
.map(s => s.toUpperCase())
.isEmpty // true
// Async version
const upper = await Optional('hello')
.mapAsync(async s => s.toUpperCase())
// upper.get() === 'HELLO'orMap / orMapAsync
Transforms the value only when the Optional is empty and not in an error state. Useful for providing computed fallbacks.
Empty<number>()
.orMap(() => 42)
.get() // 42
// Error state is preserved, orMap is skipped
Try(() => { throw new Error('!') })
.orMap(() => 42)
.isEmpty // true, error is preservedcatch / catchAsync
Handles the error when the Optional is empty and in an error state. Allows recovery from errors.
Try(() => { throw new Error('KAPOW!') })
.catch(e => 'recovered')
.get() // 'recovered'
// Async version
const result = await Try(() => { throw new Error('!') })
.catchAsync(async e => 'recovered')
// result.get() === 'recovered'Checking state
const opt = Optional('hello')
opt.isPresent // true
opt.isEmpty // false
const empty = Empty()
empty.isPresent // false
empty.isEmpty // trueError inspection
const failed = Optional(undefined, new Error('Not found'))
failed.getError() // Error('Not found')
Optional('hello').getError() // undefinedType guard
import { isOptional } from '@othree.io/optional'
isOptional(Optional('hello')) // true
isOptional('hello') // falseAutomatic flattening
Nested Optionals are flattened both at runtime and at the type level:
const nested = Optional(Optional(Optional('hello')))
nested.get() // 'hello'
// Type-level: Optional<Optional<string>> resolves to Optional<string>Chaining
Methods can be chained for expressive pipelines:
const displayName = Try(() => fetchUser())
.map(user => user.profile)
.map(profile => profile.displayName)
.catch(e => 'Anonymous')
.orElse('Guest')Three-state model
An Optional can be in one of three states:
| State | isPresent | isEmpty | getError() | Behavior |
|---|---|---|---|---|
| Present | true | false | undefined | Value is available |
| Empty | false | true | undefined | No value, no error |
| Error | false | true | Error | No value, has error |
This distinction matters for orElse, orMap, and catch:
orElse/orMapactivate on empty state, but throw on error statecatchactivates only on error statemapactivates only on present state
API reference
| Method | Signature | Description |
|---|---|---|
| get() | () => T | Extract value or throw |
| orElse(value) | (value: T) => T | Value or fallback (throws on error) |
| orThrow(error) | (error: Error) => T | Value or throw custom error |
| map(fn) | <A>(fn: (v: T) => A \| Optional<A>) => Optional<A> | Transform if present |
| mapAsync(fn) | <A>(fn: (v: T) => Promise<A \| Optional<A>>) => Promise<Optional<A>> | Async transform if present |
| orMap(fn) | <A>(fn: () => A \| Optional<A>) => Optional<A \| T> | Transform if empty (no error) |
| orMapAsync(fn) | <A>(fn: () => Promise<A \| Optional<A>>) => Promise<Optional<A \| T>> | Async transform if empty |
| catch(fn) | <A>(fn: (e: Error) => A \| Optional<A>) => Optional<A \| T> | Recover from error |
| catchAsync(fn) | <A>(fn: (e: Error) => Promise<A \| Optional<A>>) => Promise<Optional<A \| T>> | Async recover from error |
| getError() | () => any | Get the error, if any |
| isEmpty | boolean | true if no value |
| isPresent | boolean | true if value exists |
