Maybe
Type-safe optional values without null/undefined
The Maybe type is a type-safe way to handle optional values without using null or undefined. It represents either a value present (Some) or no value (None).
Why Use Maybe?
JavaScript uses null and undefined to represent missing values, but this approach has several problems:
- Runtime errors: Accessing properties on
null/undefinedthrows errors - Type safety: TypeScript's
nullandundefinedare easy to miss - Ambiguity: It's often unclear whether a value can be missing
- Falsy values:
0,"", andfalseare often confused with missing values
Maybe solves these problems by making optional values explicit in the type system.
Basic Usage
Creating Maybe Values
Use the Maybe namespace constructors:
import { Maybe } from '@deessejs/functions';
// Some - value exists
const maybe1 = Maybe.some(42);
console.log(maybe1.value); // 42
// None - no value
const maybe2 = Maybe.none();
// No value to accessType Guards
Use isSome() and isNone() to check and narrow types:
function findUser(id: number): Maybe<User> {
const user = database.find(id);
if (!user) {
return Maybe.none();
}
return Maybe.some(user);
}
const user = findUser(123);
if (user.isSome()) {
// TypeScript knows user is Some here
console.log("Found:", user.value.name);
} else {
// TypeScript knows user is None here
console.log("User not found");
}Pattern Matching
The match() method provides elegant pattern matching:
const message = user.match({
onSome: (value) => `User: ${value.name}`,
onNone: () => "User not found",
});
console.log(message);Working with Maybe
Default Values
Provide a fallback value when there's none:
const config = getConfig();
const timeout = config.match({
onSome: (cfg) => cfg.timeout,
onNone: () => 5000, // default timeout
});Transforming Values
const name = getUser()
.match({
onSome: (user) => user.name,
onNone: () => "Guest",
})
.toUpperCase();Chaining Operations
const email = findUser(123)
.match({
onSome: (user) => user.email,
onNone: () => Maybe.none(),
})
.match({
onSome: (email) => email,
onNone: () => "No email available",
});Type Safety
Maybe provides excellent type safety with TypeScript:
function getFirstItem<T>(items: T[]): Maybe<T> {
if (items.length === 0) {
return Maybe.none();
}
return Maybe.some(items[0]);
}
const result = getFirstItem([1, 2, 3]);
if (result.isSome()) {
// TypeScript knows:
// - result.value exists and is of type number
console.log(result.value + 10);
}Handling Falsy Values
Maybe correctly handles falsy values that are actually present:
// These are all valid Some values
Maybe.some(0); // number zero
Maybe.some(""); // empty string
Maybe.some(false); // boolean false
// Only Maybe.none() represents no valueThis avoids common bugs where falsy values are confused with missing values.
Best Practices
1. Always Handle Both Cases
Use match() to ensure both cases are handled:
// ✅ Good - explicit handling
maybeValue.match({
onSome: (value) => console.log(value),
onNone: () => console.log("No value"),
});
// ⚠️ Be careful - you might forget to handle None
if (maybeValue.isSome()) {
console.log(maybeValue.value);
}2. Return None for Missing Values
When a value might not exist, return Maybe:
// ❌ Bad - returns null
function findUser(id: number): User | null {
// ...
}
// ✅ Good - returns Maybe
function findUser(id: number): Maybe<User> {
// ...
}3. Don't Use Maybe for Nullable Values
If you need to distinguish between "no value" and "null value", use a different approach:
// ❌ Confusing - is Maybe.none() different from Maybe.some(null)?
type MaybeNull<T> = Maybe<T | null>;
// ✅ Better - use Result or a custom type
type Nullable<T> = T | null;4. Use Descriptive Match Handlers
Make it clear what happens in each case:
// ✅ Good - clear intent
const result = findUser(id).match({
onSome: (user) => `Welcome ${user.name}!`,
onNone: () => "User not found. Please register.",
});
// ❌ Less clear
const result = findUser(id).match({
onSome: (u) => u.name,
onNone: () => "error",
});Common Patterns
Safe Array Access
function first<T>(array: T[]): Maybe<T> {
return array.length > 0 ? Maybe.some(array[0]) : Maybe.none();
}
function last<T>(array: T[]): Maybe<T> {
return array.length > 0 ? Maybe.some(array[array.length - 1]) : Maybe.none();
}Safe Property Access
function getEmail(user: Maybe<User>): Maybe<string> {
return user.match({
onSome: (u) => u.email ? Maybe.some(u.email) : Maybe.none(),
onNone: () => Maybe.none(),
});
}Configuration with Defaults
interface Config {
timeout?: number;
retries?: number;
}
function getTimeout(config: Maybe<Config>): number {
return config.match({
onSome: (cfg) => cfg.timeout ?? 5000,
onNone: () => 5000,
});
}