Conditionals Guide
Replace if-else chains with declarative conditional combinators.
Overview
Conditional combinators let you express branching logic as composable functions instead of imperative control structures. This makes your code more declarative, testable, and easier to compose in pipelines.
ifElse - Binary Branching
The ifElse combinator is the functional equivalent of the ternary operator (? :).
Signature
function ifElse<T, U>(
predicate: (value: T) => boolean,
onTrue: (value: T) => U,
onFalse: (value: T) => U
): (value: T) => UBasic Usage
import { ifElse } from '@ontologyio/receta/function'
// Classification logic
const classify = ifElse(
(n: number) => n >= 0,
(n) => 'positive',
(n) => 'negative'
)
classify(5) // => 'positive'
classify(-3) // => 'negative'
classify(0) // => 'positive'Comparison with Traditional Code
// ❌ Traditional ternary
const result = age >= 18
? { status: 'adult', age }
: { status: 'minor', age }
// ✅ ifElse combinator (more reusable)
const classifyAge = ifElse(
(age: number) => age >= 18,
(age) => ({ status: 'adult', age }),
(age) => ({ status: 'minor', age })
)
const result = classifyAge(age)Real-World Example: User Access Control
import { pipe } from 'remeda'
import { ifElse } from '@ontologyio/receta/function'
interface User {
id: string
role: 'admin' | 'user' | 'guest'
permissions: string[]
}
interface AccessGrant {
canAccess: boolean
level: 'full' | 'limited' | 'none'
user: User
}
const grantAccess = ifElse(
(user: User) => user.role === 'admin',
(user): AccessGrant => ({
canAccess: true,
level: 'full',
user
}),
(user): AccessGrant => ({
canAccess: user.permissions.includes('read'),
level: user.permissions.length > 0 ? 'limited' : 'none',
user
})
)
const adminUser: User = { id: '1', role: 'admin', permissions: [] }
const regularUser: User = { id: '2', role: 'user', permissions: ['read'] }
console.log(grantAccess(adminUser))
// => { canAccess: true, level: 'full', user: ... }
console.log(grantAccess(regularUser))
// => { canAccess: true, level: 'limited', user: ... }when - Conditional Transformation
The when combinator applies a transformation only when a condition is met; otherwise, it returns the original value.
Signature
function when<T>(
predicate: (value: T) => boolean,
fn: (value: T) => T
): (value: T) => TBasic Usage
import { when } from '@ontologyio/receta/function'
// Only transform negative numbers
const ensurePositive = when(
(n: number) => n < 0,
(n) => Math.abs(n)
)
ensurePositive(-5) // => 5
ensurePositive(3) // => 3
ensurePositive(0) // => 0Chaining Multiple when Calls
import { piped } from 'remeda'
const normalizeText = piped(
when(
(s: string) => s.length === 0,
() => 'default'
),
when(
(s: string) => s.startsWith(' ') || s.endsWith(' '),
(s) => s.trim()
),
when(
(s: string) => s.toLowerCase() === s,
(s) => s.charAt(0).toUpperCase() + s.slice(1)
)
)
normalizeText('') // => 'Default'
normalizeText(' hello ') // => 'Hello'
normalizeText('world') // => 'World'
normalizeText('CAPS') // => 'CAPS' (already has uppercase)Real-World Example: Form Sanitization
import { pipe } from 'remeda'
import { when } from '@ontologyio/receta/function'
interface FormData {
email: string
username: string
bio?: string
}
const sanitizeForm = (data: FormData): FormData =>
pipe(
data,
// Normalize email
when(
(d) => d.email.includes(' '),
(d) => ({ ...d, email: d.email.trim().toLowerCase() })
),
// Add default bio if missing
when(
(d) => !d.bio || d.bio.length === 0,
(d) => ({ ...d, bio: 'No bio provided' })
),
// Truncate long bios
when(
(d) => d.bio !== undefined && d.bio.length > 500,
(d) => ({ ...d, bio: d.bio!.slice(0, 497) + '...' })
)
)
const form: FormData = {
email: ' Alice@Example.com ',
username: 'alice',
bio: 'A'.repeat(600)
}
console.log(sanitizeForm(form))
// => {
// email: 'alice@example.com',
// username: 'alice',
// bio: 'AAA...AAA...' (truncated to 500 chars)
// }unless - Inverse Conditional
The unless combinator is the inverse of when: it applies a transformation when the condition is not met.
Signature
function unless<T>(
predicate: (value: T) => boolean,
fn: (value: T) => T
): (value: T) => TBasic Usage
import { unless } from '@ontologyio/receta/function'
// Ensure value is wrapped in array
const ensureArray = unless(
Array.isArray,
(value) => [value]
)
ensureArray([1, 2, 3]) // => [1, 2, 3]
ensureArray(5) // => [5]
ensureArray('hello') // => ['hello']When to Use when vs unless
Use when if the condition reads naturally:
// ✅ Reads clearly
when(isEmpty, fillWithDefault)
when(isNegative, makePositive)Use unless if the inverse reads more naturally:
// ✅ More natural to read
unless(hasProtocol, addProtocol)
unless(isAuthenticated, redirectToLogin)
// ❌ Less natural
when((user) => !isAuthenticated(user), redirectToLogin)Real-World Example: Configuration Defaults
import { pipe } from 'remeda'
import { unless } from '@ontologyio/receta/function'
interface Config {
apiUrl?: string
timeout?: number
retries?: number
apiKey?: string
}
const applyDefaults = (config: Config): Config =>
pipe(
config,
unless(
(c) => 'apiUrl' in c && c.apiUrl !== undefined,
(c) => ({ ...c, apiUrl: 'https://api.example.com' })
),
unless(
(c) => 'timeout' in c && c.timeout !== undefined,
(c) => ({ ...c, timeout: 5000 })
),
unless(
(c) => 'retries' in c && c.retries !== undefined,
(c) => ({ ...c, retries: 3 })
),
unless(
(c) => 'apiKey' in c && c.apiKey !== undefined,
(c) => ({ ...c, apiKey: process.env.API_KEY || 'default-key' })
)
)
const userConfig: Config = { timeout: 10000 }
console.log(applyDefaults(userConfig))
// => {
// apiUrl: 'https://api.example.com',
// timeout: 10000, // User-provided value preserved
// retries: 3,
// apiKey: 'default-key'
// }cond - Multi-Way Conditional
The cond combinator handles multiple conditions in sequence, returning Some with the first match or None if no conditions match.
Signature
type CondPair<T, U> = readonly [
predicate: (value: T) => boolean,
fn: (value: T) => U
]
function cond<T, U>(
pairs: readonly CondPair<T, U>[]
): (value: T) => Option<U>Basic Usage
import { cond } from '@ontologyio/receta/function'
import { unwrapOr } from '@ontologyio/receta/option'
const classifyNumber = cond<number, string>([
[(n) => n < 0, (n) => 'negative'],
[(n) => n === 0, () => 'zero'],
[(n) => n > 0 && n < 10, (n) => 'small positive'],
[(n) => n >= 10, (n) => 'large positive']
])
unwrapOr(classifyNumber(-5), 'unknown') // => 'negative'
unwrapOr(classifyNumber(0), 'unknown') // => 'zero'
unwrapOr(classifyNumber(3), 'unknown') // => 'small positive'
unwrapOr(classifyNumber(100), 'unknown') // => 'large positive'Comparison with Switch Statement
// ❌ Traditional switch
function getStatusMessageSwitch(code: number): string {
switch (true) {
case code >= 200 && code < 300:
return 'Success'
case code >= 300 && code < 400:
return 'Redirect'
case code >= 400 && code < 500:
return 'Client Error'
case code >= 500:
return 'Server Error'
default:
return 'Unknown'
}
}
// ✅ cond combinator (more functional)
const getStatusMessage = cond<number, string>([
[(code) => code >= 200 && code < 300, () => 'Success'],
[(code) => code >= 300 && code < 400, () => 'Redirect'],
[(code) => code >= 400 && code < 500, () => 'Client Error'],
[(code) => code >= 500, () => 'Server Error']
])
const message = unwrapOr(getStatusMessage(404), 'Unknown')Real-World Example: State Machine
import { cond } from '@ontologyio/receta/function'
import { unwrapOr } from '@ontologyio/receta/option'
type OrderState =
| 'pending'
| 'processing'
| 'shipped'
| 'delivered'
| 'cancelled'
interface Order {
id: string
state: OrderState
items: number
total: number
}
interface StateAction {
message: string
canCancel: boolean
nextStates: OrderState[]
}
const getOrderAction = cond<Order, StateAction>([
[
(order) => order.state === 'pending',
(order) => ({
message: `Order ${order.id} is pending payment`,
canCancel: true,
nextStates: ['processing', 'cancelled']
})
],
[
(order) => order.state === 'processing',
(order) => ({
message: `Processing ${order.items} items (total: $${order.total})`,
canCancel: true,
nextStates: ['shipped', 'cancelled']
})
],
[
(order) => order.state === 'shipped',
(order) => ({
message: `Order ${order.id} is on the way`,
canCancel: false,
nextStates: ['delivered']
})
],
[
(order) => order.state === 'delivered',
(order) => ({
message: `Order ${order.id} was delivered`,
canCancel: false,
nextStates: []
})
],
[
(order) => order.state === 'cancelled',
(order) => ({
message: `Order ${order.id} was cancelled`,
canCancel: false,
nextStates: []
})
]
])
const order: Order = {
id: 'ORD-123',
state: 'processing',
items: 3,
total: 99.99
}
const action = unwrapOr(getOrderAction(order), {
message: 'Unknown state',
canCancel: false,
nextStates: []
})
console.log(action)
// => {
// message: 'Processing 3 items (total: $99.99)',
// canCancel: true,
// nextStates: ['shipped', 'cancelled']
// }Handling No Matches
Since cond returns an Option, you always handle the "no match" case explicitly:
import { cond } from '@ontologyio/receta/function'
import { match } from '@ontologyio/receta/option'
const result = pipe(
value,
cond(pairs),
match({
onSome: (value) => `Matched: ${value}`,
onNone: () => 'No condition matched'
})
)Combining Conditionals
You can combine different conditional combinators for complex logic:
import { pipe } from 'remeda'
import { when, unless, ifElse } from '@ontologyio/receta/function'
interface Product {
price: number
inStock: boolean
category: string
}
const applyPricing = (product: Product) =>
pipe(
product,
// Add tax unless exempt category
unless(
(p) => p.category === 'essential',
(p) => ({ ...p, price: p.price * 1.2 })
),
// Round to 2 decimals
when(
(p) => p.price % 1 !== 0,
(p) => ({ ...p, price: Math.round(p.price * 100) / 100 })
),
// Apply discount based on stock
(p) => ({
...p,
price: ifElse(
(prod: Product) => prod.inStock,
(prod) => prod.price,
(prod) => prod.price * 0.9 // 10% discount if out of stock
)(p)
})
)Best Practices
1. Keep Predicates Pure
// ❌ Avoid side effects in predicates
let counter = 0
when((n) => { counter++; return n > 0 }, transform)
// ✅ Pure predicates
when((n) => n > 0, transform)2. Use Descriptive Names
// ❌ Unclear
const fn = when((x: number) => x < 0, (x) => -x)
// ✅ Clear intent
const ensurePositive = when(
(n: number) => n < 0,
(n) => Math.abs(n)
)3. Order cond Pairs Carefully
// ✅ Specific conditions first
cond<number, string>([
[(n) => n === 0, () => 'zero'], // Most specific
[(n) => n > 0 && n < 10, () => 'small'], // More specific
[(n) => n > 0, () => 'positive'], // Less specific
[() => true, () => 'other'] // Catch-all last
])
// ❌ General conditions first will shadow specific ones
cond<number, string>([
[(n) => n > 0, () => 'positive'], // Too general
[(n) => n > 0 && n < 10, () => 'small'], // Never reached!
])4. Prefer unless for Negated Conditions
// ❌ Double negative is confusing
when((user) => !user.isAuthenticated, redirectToLogin)
// ✅ Positive reading
unless((user) => user.isAuthenticated, redirectToLogin)Type Safety
All conditional combinators maintain full type safety:
const result = ifElse(
(n: number) => n > 0,
(n) => `positive: ${n}`, // n is inferred as number
(n) => `negative: ${n}` // n is inferred as number
)
// result type: (n: number) => stringTypeScript will catch type mismatches:
ifElse(
(n: number) => n > 0,
(n) => n.toString(), // returns string
(n) => n // ❌ Error: must return string
)Next Steps
- Composition Guide - Learn compose, converge, juxt
- Integration Guide - Combine with Result and Option
- API Reference - Complete function signatures