receta
Option

Option Transformers

Functions that transform Option values while preserving the Some/None state.

Overview

Transformers let you work with values inside Options without manual unwrapping. They handle the Some/None cases automatically.

FunctionPurposeSignature
mapTransform the valueOption<T> → (T → U) → Option<U>
flatMapChain Option-returning functionsOption<T> → (T → Option<U>) → Option<U>
filterKeep value if predicate passesOption<T> → (T → boolean) → Option<T>
flattenUnwrap nested OptionsOption<Option<T>> → Option<T>

All transformers support data-first and data-last (pipe) usage via Remeda's purry.

map()

Transform the value inside an Option.

Signature

Signature
// Data-first
map<T, U>(option: Option<T>, fn: (value: T) => U): Option<U>

// Data-last
map<T, U>(fn: (value: T) => U): (option: Option<T>) => Option<U>

Basic Usage

import { map, some, none } from '@ontologyio/receta/option'

// Transform Some
map(some(5), x => x * 2)
// => Some(10)

// None stays None
map(none(), (x: number) => x * 2)
// => None

In Pipelines

import * as R from 'remeda'

const result = R.pipe(
  some(5),
  map(x => x * 2),      // Some(10)
  map(x => x + 1),      // Some(11)
  map(x => String(x))   // Some('11')
)

Real-World: Database + Transformation

// Find user and get their email
const getUserEmail = (id: string): Option<string> =>
  R.pipe(
    findUserById(id),
    map(user => user.email),
    map(email => email.toLowerCase())
  )

getUserEmail('123')  // => Some('alice@example.com')
getUserEmail('999')  // => None

Real-World: API Response Formatting

// GitHub API - format user display name
type GitHubUser = {
  login: string
  name: string | null
  followers: number
}

const getDisplayName = (user: GitHubUser): string =>
  R.pipe(
    fromNullable(user.name),
    map(name => `${name} (@${user.login})`),
    unwrapOr(user.login)
  )

getDisplayName({ login: 'torvalds', name: 'Linus Torvalds', followers: 1000 })
// => 'Linus Torvalds (@torvalds)'

getDisplayName({ login: 'octocat', name: null, followers: 500 })
// => 'octocat'

flatMap()

Chain operations that return Options, avoiding nested Option<Option<T>>.

Signature

Signature
// Data-first
flatMap<T, U>(option: Option<T>, fn: (value: T) => Option<U>): Option<U>

// Data-last
flatMap<T, U>(fn: (value: T) => Option<U>): (option: Option<T>) => Option<U>

Basic Usage

import { flatMap, some, none } from '@ontologyio/receta/option'

// Function returns Option
const half = (x: number) => x % 2 === 0 ? some(x / 2) : none()

flatMap(some(10), half) // => Some(5)
flatMap(some(11), half) // => None
flatMap(none(), half)   // => None

Real-World: Nested Lookups

// User has optional team, team has optional lead
type User = { id: string; teamId?: string }
type Team = { id: string; leadId?: string }

const findTeamLead = (userId: string): Option<User> =>
  R.pipe(
    findUserById(userId),
    flatMap(user =>
      fromNullable(user.teamId)
    ),
    flatMap(teamId =>
      findTeamById(teamId)
    ),
    flatMap(team =>
      fromNullable(team.leadId)
    ),
    flatMap(leadId =>
      findUserById(leadId)
    )
  )

// Returns Some(lead) if all lookups succeed, None if any fail

Real-World: Validation Chains

const validateAge = (age: number): Option<number> =>
  age >= 18 && age <= 120 ? some(age) : none()

const parseAndValidateAge = (input: string): Option<number> =>
  R.pipe(
    tryCatch(() => Number(input)),
    filter(n => !Number.isNaN(n)),
    flatMap(validateAge)
  )

parseAndValidateAge('25')  // => Some(25)

// Too young
parseAndValidateAge('15')  // => None

// Not a number
parseAndValidateAge('abc') // => None

filter()

Keep the value only if a predicate passes.

Signature

Signature
// Data-first
filter<T>(option: Option<T>, predicate: (value: T) => boolean): Option<T>

// Data-last
filter<T>(predicate: (value: T) => boolean): (option: Option<T>) => Option<T>

Basic Usage

import { filter, some, none } from '@ontologyio/receta/option'

filter(some(5), x => x > 0)   // => Some(5)
filter(some(-5), x => x > 0)  // => None
filter(none(), (x: number) => x > 0)    // => None

Real-World: Form Validation

// Validate email format
const validateEmail = (email: string): Option<string> =>
  R.pipe(
    fromNullable(email),
    filter(e => e.length >= 3),
    filter(e => e.includes('@')),
    filter(e => e.includes('.'))
  )

validateEmail('user@example.com')  // => Some('user@example.com')
validateEmail('invalid')            // => None
validateEmail('')                   // => None

Real-World: Configuration Validation

// Parse and validate timeout config
const getTimeout = (): number =>
  R.pipe(
    fromNullable(process.env.TIMEOUT),
    map(s => Number(s)),
    filter(n => !Number.isNaN(n)),
    filter(n => n > 0),
    filter(n => n <= 60000),
    unwrapOr(5000) // Default 5 seconds
  )

flatten()

Remove one level of nesting from Option<Option<T>>.

Signature

Signature
// Data-first
flatten<T>(option: Option<Option<T>>): Option<T>

// Data-last
flatten<T>(): (option: Option<Option<T>>) => Option<T>

Basic Usage

import { flatten, some, none } from '@ontologyio/receta/option'

flatten(some(some(42)))  // => Some(42)
flatten(some(none()))    // => None
flatten(none())          // => None

Real-World: Avoiding Nesting

// Without flatten - nested Option
const lookup1 = (id: string): Option<Option<User>> =>
  R.pipe(
    findTeamById(id),
    map(team => findUserById(team.leadId))
  )
// Returns Option<Option<User>> - awkward!

// With flatten
const lookup2 = (id: string): Option<User> =>
  R.pipe(
    findTeamById(id),
    map(team => findUserById(team.leadId)),
    flatten()
  )
// Returns Option<User> - clean!

// Or use flatMap instead
const lookup3 = (id: string): Option<User> =>
  R.pipe(
    findTeamById(id),
    flatMap(team => findUserById(team.leadId))
  )

Combining Transformers

Pipeline Example

const processUserId = (idStr: string): Option<User> =>
  R.pipe(
    // Parse ID
    tryCatch(() => Number(idStr)),
    // Validate it's positive
    filter(id => id > 0),
    // Convert to string
    map(id => String(id)),
    // Look up user
    flatMap(id => findUserById(id)),
    // Filter active users only
    filter(user => user.active)
  )

processUserId('123')   // Some(user) if found and active
processUserId('-1')    // None (negative ID)
processUserId('abc')   // None (parse failed)

Cheat Sheet

// Transform value
map(option, x => x * 2)

// Chain Option-returning functions
flatMap(option, x => lookup(x))

// Keep if predicate passes
filter(option, x => x > 0)

// Flatten nested Options
flatten(nestedOption)

// In pipeline
R.pipe(
  some(5),
  map(x => x * 2),
  filter(x => x > 5),
  flatMap(x => lookup(x))
)

When to Use Which

ScenarioUse
Transform the valuemap
Value doesn't change typemap
Next operation returns OptionflatMap
Conditional keepingfilter
Have Option<Option<T>>flatten or use flatMap instead

Next Steps

On this page