receta
Option

Option Patterns & Recipes

Production-ready patterns for common use cases.

Database Queries

findById Pattern

type User = { id: string; name: string; email: string }

const findUserById = (id: string): Option<User> =>
  fromNullable(users.find(u => u.id === id))

const getUserEmail = (id: string): string =>
  R.pipe(
    findUserById(id),
    map(u => u.email),
    unwrapOr('noreply@example.com')
  )

Nested Lookups

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

Configuration Loading

Environment Variables with Defaults

const getEnv = (key: string): Option<string> =>
  fromNullable(process.env[key])

const config = {
  apiKey: unwrapOr(getEnv('API_KEY'), 'default-key'),
  timeout: pipe(
    getEnv('TIMEOUT'),
    map(Number),
    filter(n => !isNaN(n) && n > 0),
    unwrapOr(5000)
  ),
  debug: unwrapOr(getEnv('DEBUG'), 'false') === 'true'
}

Feature Flags

const getFeatureFlag = (name: string): boolean =>
  pipe(
    fromNullable(featureFlags[name]),
    unwrapOr(false)
  )

if (getFeatureFlag('new-ui')) {
  renderNewUI()
} else {
  renderLegacyUI()
}

Form Validation

Multi-Field Validation

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

const validateAge = (age: string): Option<number> =>
  pipe(
    tryCatch(() => Number(age)),
    filter(n => !isNaN(n)),
    filter(n => n >= 18 && n <= 120)
  )

const validateForm = (form: SignupForm): Option<ValidForm> =>
  pipe(
    validateEmail(form.email),
    flatMap(email => map(validateAge(form.age), age => ({ email, age })))
  )

collect needs every Option to hold the same type. Here one holds a string and the other a number, so flatMap combines them instead.

Optional Fields

type ProfileForm = {
  name: string  // Required
  email: string  // Required
  phone?: string  // Optional
  website?: string  // Optional
}

const processForm = (form: ProfileForm) => ({
  name: form.name,
  email: form.email,
  phone: pipe(
    fromNullable(form.phone),
    filter(p => /^\d{10}$/.test(p)),
    toNullable()
  ),
  website: pipe(
    fromNullable(form.website),
    filter(w => w.startsWith('http')),
    toNullable()
  )
})

API Integration

GitHub API

type GitHubUser = {
  login: string
  name: string | null
  bio: string | null
  company: string | null
}

const formatUser = (user: GitHubUser) => ({
  username: user.login,
  displayName: unwrapOr(fromNullable(user.name), user.login),
  bio: fromNullable(user.bio),
  company: fromNullable(user.company)
})

Stripe API

type PaymentIntent = {
  id: string
  amount: number
  metadata?: Record<string, string>
}

const getMetadata = (intent: PaymentIntent, key: string): Option<string> =>
  pipe(
    fromNullable(intent.metadata),
    flatMap(meta => fromNullable(meta[key]))
  )

const orderId = unwrapOr(
  getMetadata(intent, 'order_id'),
  'unknown'
)

Caching

With Fallback

const cache = new Map<string, User>()

const getUser = (id: string): Promise<User> =>
  match(fromNullable(cache.get(id)), {
    onSome: user => Promise.resolve(user),
    onNone: () => fetchUserFromDB(id).then(user => {
      cache.set(id, user)
      return user
    })
  })

Parsing

JSON with Validation

const parseConfig = <T>(str: string, validate: (x: unknown) => x is T): Option<T> =>
  flatMap(tryCatch(() => JSON.parse(str)), value =>
    validate(value) ? some(value) : none()
  )

const config = parseConfig(configStr, isValidConfig)

Number Parsing

const parsePositiveInt = (str: string): Option<number> =>
  pipe(
    tryCatch(() => Number(str)),
    filter(n => !isNaN(n)),
    filter(n => Number.isInteger(n)),
    filter(n => n > 0)
  )

Batch Processing

const processUserIds = (ids: string[]): User[] => {
  const results = ids.map(findUserById)
  const [users, failedCount] = partition(results)

  logger.info(`Processed ${users.length}, Failed: ${failedCount}`)
  return users
}

Integration with Result

Convert for Detailed Errors

const getUserOrError = (id: string): Result<User, NotFoundError> =>
  pipe(
    findUserById(id),
    toResult({ code: 'NOT_FOUND', userId: id })
  )

Discard Error Details

const hasUser = (id: string): boolean =>
  pipe(
    validateAndFetchUser(id), // Returns Result
    fromResult,
    isSome
  )

Next Steps

On this page