Maybe
Explicit optional values with Some and None
The Maybe type represents a value that can be either present (Some) or absent (None). It's the functional alternative to null and undefined.
Why Maybe?
TypeScript's null and undefined are the source of many runtime errors. When a value can be absent, the type system should make that explicit.
The Problem with Null
// This could be undefined, but the type doesn't tell us
const user = users.find(u => u.id === id);
// Type: User | undefined
// No compiler warning if you forget to check
console.log(user.name); // Runtime error: Cannot read property 'name' of undefinedThe Maybe Solution
With Maybe, absence becomes explicit in your types:
import { some, fromNullable, isSome, Maybe } from '@deessejs/fp';
const user: Maybe<User> = fromNullable(users.find(u => u.id === id));
if (isSome(user)) {
console.log(user.value.name); // TypeScript knows it's safe
}Key concepts:
Maybe<T>- Two states:Some<T>(present) orNone(absent)some(value)- Wraps a present value:{ ok: true, value: T }none()- Represents absence:{ ok: false }fromNullable(value)- Converts null/undefined to None, other values to SomeisSome()- Type guard that narrows to the success branch
Core Concepts
Some and None
Maybe<T> has two possible states:
| State | Description | Properties |
|---|---|---|
Some<T> | Present value | ok: true, value: T |
None | Absent value | ok: false |
import { some, none, Maybe } from '@deessejs/fp';
// Present value
const present: Maybe<number> = some(42);
// Absent value
const absent: Maybe<number> = none();The Difference from null
fromNullable only treats null and undefined as absent. This is intentional - 0, '', and false are valid values:
import { fromNullable } from '@deessejs/fp';
fromNullable(42); // Some(42)
fromNullable(null); // None
fromNullable(undefined); // None
fromNullable(0); // Some(0) - NOT None!
fromNullable(''); // Some('') - NOT None!
fromNullable(false); // Some(false) - NOT None!Reference
Creating Maybes
some(value)
Creates a Some with a present value. The value must be non-null and non-undefined.
import { some } from '@deessejs/fp';
some(42); // Some(42)
some('hello'); // Some('hello')
some({ id: 1 }); // Some({ id: 1 })none()
Creates a None representing absent value.
import { none } from '@deessejs/fp';
none(); // NoneNote: none() returns a singleton instance, so comparisons like none() === none() work correctly.
fromNullable(value)
Converts a nullable value to Maybe. Returns Some if the value is not null or undefined, None otherwise.
import { fromNullable } from '@deessejs/fp';
fromNullable(42); // Some(42)
fromNullable(null); // None
fromNullable(undefined); // NoneType Guards
isSome(maybe)
Type guard that narrows to the Some branch.
import { some, isSome } from '@deessejs/fp';
const result = some(42);
if (isSome(result)) {
console.log(result.value); // 42
}isNone(maybe)
Type guard that narrows to the None branch.
import { none, isNone } from '@deessejs/fp';
const result = none();
if (isNone(result)) {
console.log('No value');
}Transformation
map(maybe, fn)
Transforms the value if Some. Passes None through unchanged.
import { some, none, map } from '@deessejs/fp';
map(some(2), x => x * 2); // Some(4)
map(none(), x => x * 2); // NoneAlso available as a method:
some(2).map(x => x * 2); // Some(4)flatMap(maybe, fn)
Chains operations that return Maybe. If Some, applies the function. If None, returns None.
import { some, none, flatMap } from '@deessejs/fp';
const findUser = (id: number) =>
id > 0 ? some({ id, name: 'John' }) : none();
const getEmail = (user: { name: string }) =>
some(user.name.toLowerCase() + '@example.com');
flatMap(some(1), findUser); // Some({ id: 1, name: 'John' })
flatMap(some(-1), findUser); // None
flatMap(some(1), x => flatMap(findUser(x.id), getEmail)); // Some('john@example.com')Also available as a method:
some(1).flatMap(findUser); // Some({ id: 1, name: 'John' })filter(maybe, predicate)
Returns Some if the predicate passes. Returns None if the predicate fails or if already None.
import { some, none, filter } from '@deessejs/fp';
filter(some(42), x => x > 10); // Some(42)
filter(some(5), x => x > 10); // None
filter(none(), x => x > 10); // NoneAlso available as a method:
some(42).filter(x => x > 10); // Some(42)flatten(maybe)
Flattens a nested Maybe. Turns Maybe<Maybe<T>> into Maybe<T>.
import { some, flatten } from '@deessejs/fp';
flatten(some(some(42))); // Some(42)
flatten(some(none())); // None
flatten(none()); // NoneAlso available as a method:
some(some(42)).flatten(); // Some(42)Extraction
getOrElse(maybe, defaultValue)
Returns the value if Some, or a default if None.
import { some, none, getOrElse } from '@deessejs/fp';
getOrElse(some(42), 0); // 42
getOrElse(none(), 0); // 0Also available as a method:
some(42).getOrElse(0); // 42
none().getOrElse(0); // 0getOrCompute(maybe, fn)
Returns the value if Some, or computes one lazily if None. Useful for expensive fallbacks.
import { some, none, getOrCompute } from '@deessejs/fp';
const expensive = () => { console.log('computing...'); return 0; };
getOrCompute(some(42), expensive); // 42 (never logs)
getOrCompute(none(), expensive); // logs 'computing...', returns 0Also available as a method:
none().getOrCompute(() => 0); // 0Side Effects
tap(maybe, fn)
Executes a function on the value without changing it. Useful for logging.
import { some, none, tap } from '@deessejs/fp';
tap(some(42), x => console.log('Got:', x)); // Logs: 'Got: 42', returns Some(42)
tap(none(), x => console.log('Got:', x)); // Nothing logged, returns NoneAlso available as a method:
some(42).tap(x => console.log(x)); // Some(42)Pattern Matching
match(maybe, someFn, noneFn)
Handles both Some and None cases, returning a single value.
import { some, none, match } from '@deessejs/fp';
match(some(42), x => x * 2, () => 0); // 84
match(none(), x => x * 2, () => 0); // 0Returns different types:
match(
some('hello'),
value => value.length, // string -> number
() => -1 // fallback
); // 5Also available as a method with object syntax:
some(42).match({ onSome: x => x * 2, onNone: () => 0 }); // 84
none().match({ onSome: x => x * 2, onNone: () => 0 }); // 0Conversion
toNullable(maybe)
Converts to T | null.
import { some, none, toNullable } from '@deessejs/fp';
toNullable(some(42)); // 42
toNullable(none()); // nulltoUndefined(maybe)
Converts to T | undefined.
import { some, none, toUndefined } from '@deessejs/fp';
toUndefined(some(42)); // 42
toUndefined(none()); // undefinedtoResult(maybe, onNone)
Converts to Result<T, Error>. Some becomes Ok, None becomes Err using the provided error.
import { some, none, toResult, error } from '@deessejs/fp';
import { z } from 'zod';
const NotFoundError = error({
name: 'NotFoundError',
schema: z.object({ key: z.string() }),
message: (args) => `Not found: ${args.key}`,
});
toResult(some(42), () => NotFoundError({ key: 'value' })); // Ok(42)
toResult(none(), () => NotFoundError({ key: 'value' })); // Err(NotFoundError({ key: 'value' }))For more on structured errors, see Error.
Comparison
equals(a, b)
Compares two Maybes for equality. Both Some with equal values or both None return true.
import { some, none, equals } from '@deessejs/fp';
equals(some(42), some(42)); // true
equals(some(42), some(10)); // false
equals(some(42), none()); // false
equals(none(), none()); // trueAlso available as a method:
some(42).equals(some(42)); // trueequalsWith(a, b, comparator)
Compares two Maybes using a custom comparator function.
import { some, equalsWith } from '@deessejs/fp';
equalsWith(
some({ id: 1, name: 'John' }),
some({ id: 2, name: 'John' }),
(a, b) => a.name === b.name
); // true (comparing by name only)Combination
all(...maybes)
Combines multiple Maybes into one. Returns Some with array of values if all are Some, or None if any is None (fail-fast).
import { some, none, all } from '@deessejs/fp';
all(some(1), some(2), some(3)); // Some([1, 2, 3])
all(some(1), none(), some(3)); // NoneAlso supports passing an array:
all([some(1), some(2), some(3)]); // Some([1, 2, 3])Method Chaining
Maybe objects support chaining for fluent transformations:
import { some, none, fromNullable } from '@deessejs/fp';
const result = some(5)
.map(x => x * 2) // Some(10)
.map(x => x + 1) // Some(11)
.filter(x => x > 10) // Some(11)
.flatMap(x => x > 10 ? some(x) : none()); // Some(11)Real-World Examples
Finding an Item in a List
import { fromNullable, getOrElse } from '@deessejs/fp';
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
const findUser = (id: number): Maybe<User> =>
fromNullable(users.find(u => u.id === id));
const user = getOrElse(findUser(1), { id: 0, name: 'Guest' });
// { id: 1, name: 'Alice' }
const unknown = getOrElse(findUser(999), { id: 0, name: 'Guest' });
// { id: 0, name: 'Guest' }Optional Configuration
import { fromNullable, map } from '@deessejs/fp';
interface Config {
apiUrl: string;
timeout: number;
}
const getConfig = (): Partial<Config> => {
return { apiUrl: 'https://api.example.com' };
};
const maybeConfig = fromNullable(getConfig());
const withTimeout = map(maybeConfig, config => ({
...config,
timeout: config.timeout ?? 5000,
}));
// Some({ apiUrl: '...', timeout: 5000 }) if config existsChained Property Access
import { some, none, flatMap, fromNullable } from '@deessejs/fp';
interface Company {
name: string;
address?: {
city?: string;
};
}
const getCity = (company: Maybe<Company>): Maybe<string> =>
flatMap(company, c =>
flatMap(fromNullable(c.address), a =>
fromNullable(a.city)
)
);
const company: Maybe<Company> = some({
name: 'Acme',
address: { city: 'Paris' }
});
getCity(company); // Some('Paris')
const companyWithoutCity: Maybe<Company> = some({
name: 'Acme',
address: {}
});
getCity(companyWithoutCity); // NoneMaybe vs Result
| Aspect | Maybe | Result |
|---|---|---|
| Purpose | Value may or may not exist | Operation succeeded or failed |
| None vs Err | Absence of value | Failure with error reason |
| Use case | Optional fields, lookups | API calls, validation, file ops |
// Maybe: for optional values that may not exist
const user = findUser(id); // Maybe<User>
// Result: for operations that can fail
const user = fetchUser(id); // Result<User, FetchError>See Also
- Result - For operations that can fail with an error
- Try - For wrapping sync functions that might throw
- AsyncResult - For async operations with error handling
- Error - For structured errors with enrichment