Patrón Maybe/Option: Adiós null y undefined
design-patternsfunctionalmaybe-optiontypescript

¿Qué es el Patrón Maybe/Option?
Maybe (también conocido como Option) es un contenedor que puede tener un valor (Some/Just) o estar vacío (None/Nothing). Hace explícita la ausencia de valor y fuerza su manejo.
classDiagram
class Maybe~T~ {
<<abstract>>
+map(fn): Maybe~U~
+flatMap(fn): Maybe~U~
+getOrElse(default): T
+match(handlers): R
}
class Some~T~ {
-value: T
+map(fn): Some~U~
+getOrElse(default): T
}
class None {
+map(fn): None
+getOrElse(default): T
}
Maybe <|-- Some
Maybe <|-- NoneEl Problema con null/undefined
// ❌ Código defensivo por todas partes
function getDiscountedPrice(userId: string): number {
const user = findUser(userId);
if (!user) return 0;
const membership = user.membership;
if (!membership) return 0;
const discount = membership.discount;
if (!discount) return 0;
return calculateDiscount(discount);
}
// ❌ Runtime errors
function greetUser(user: User | null) {
console.log(user.name.toUpperCase()); // 💥 Cannot read property 'name' of null
}Implementación de Maybe
// maybe.ts
abstract class Maybe<T> {
abstract map<U>(fn: (value: T) => U): Maybe<U>;
abstract flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U>;
abstract getOrElse(defaultValue: T): T;
abstract getOrThrow(error?: Error): T;
abstract match<R>(handlers: { some: (value: T) => R; none: () => R }): R;
abstract isSome(): this is Some<T>;
abstract isNone(): this is None<T>;
// Factory methods
static some<T>(value: T): Maybe<T> {
return new Some(value);
}
static none<T>(): Maybe<T> {
return new None();
}
static fromNullable<T>(value: T | null | undefined): Maybe<T> {
return value != null ? Maybe.some(value) : Maybe.none();
}
}
class Some<T> extends Maybe<T> {
constructor(private readonly value: T) {
super();
}
map<U>(fn: (value: T) => U): Maybe<U> {
return Maybe.some(fn(this.value));
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
return fn(this.value);
}
getOrElse(_defaultValue: T): T {
return this.value;
}
getOrThrow(_error?: Error): T {
return this.value;
}
match<R>(handlers: { some: (value: T) => R; none: () => R }): R {
return handlers.some(this.value);
}
isSome(): this is Some<T> { return true; }
isNone(): this is None<T> { return false; }
}
class None<T> extends Maybe<T> {
map<U>(_fn: (value: T) => U): Maybe<U> {
return Maybe.none();
}
flatMap<U>(_fn: (value: T) => Maybe<U>): Maybe<U> {
return Maybe.none();
}
getOrElse(defaultValue: T): T {
return defaultValue;
}
getOrThrow(error?: Error): T {
throw error ?? new Error('Called getOrThrow on None');
}
match<R>(handlers: { some: (value: T) => R; none: () => R }): R {
return handlers.none();
}
isSome(): this is Some<T> { return false; }
isNone(): this is None<T> { return true; }
}Uso Práctico
Búsqueda en Base de Datos
// Repository que retorna Maybe
class UserRepository {
async findById(id: string): Promise<Maybe<User>> {
const user = await prisma.user.findUnique({ where: { id } });
return Maybe.fromNullable(user);
}
async findByEmail(email: string): Promise<Maybe<User>> {
const user = await prisma.user.findUnique({ where: { email } });
return Maybe.fromNullable(user);
}
}
// Uso limpio
const userResult = await userRepo.findById(userId);
const greeting = userResult.match({
some: user => `Hola, ${user.name}!`,
none: () => 'Usuario no encontrado'
});Chaining con flatMap
// ✅ Composición elegante
async function getDiscountedPrice(userId: string): Promise<Maybe<number>> {
return (await userRepo.findById(userId))
.flatMap(user => Maybe.fromNullable(user.membership))
.flatMap(membership => Maybe.fromNullable(membership.discount))
.map(discount => calculateDiscount(discount));
}
// Uso
const price = await getDiscountedPrice(userId);
const finalPrice = price.getOrElse(0);Acceso a Propiedades Anidadas
// ❌ Sin Maybe - null checks anidados
const street = user?.address?.street?.name ?? 'Unknown';
// ✅ Con Maybe - expresivo y type-safe
const street = Maybe.fromNullable(user)
.flatMap(u => Maybe.fromNullable(u.address))
.flatMap(a => Maybe.fromNullable(a.street))
.map(s => s.name)
.getOrElse('Unknown');Combinando Múltiples Maybes
// Función helper para combinar
function combine<A, B, R>(
maybeA: Maybe<A>,
maybeB: Maybe<B>,
fn: (a: A, b: B) => R
): Maybe<R> {
return maybeA.flatMap(a =>
maybeB.map(b => fn(a, b))
);
}
// Uso
const maybeFullName = combine(
Maybe.fromNullable(firstName),
Maybe.fromNullable(lastName),
(first, last) => `${first} ${last}`
);Maybe vs Optional Chaining
// Optional chaining (?.) es útil pero limitado
const name = user?.profile?.name ?? 'Guest';
// Maybe ofrece más control
const name = Maybe.fromNullable(user)
.flatMap(u => Maybe.fromNullable(u.profile))
.map(p => p.name)
.match({
some: name => name,
none: () => {
logger.warn('Profile not found');
return 'Guest';
}
});Comparación
| Aspecto | null/undefined | Maybe/Option |
|---|---|---|
| Explícito | No | Sí |
| Type-safe | Parcial | Total |
| Composición | Difícil | Fácil |
| Runtime errors | Posibles | Eliminados |
| Verbose | null checks | Métodos chainables |
Cuándo Usar
| Situación | Recomendación |
|---|---|
| Búsquedas en BD | ✅ Ideal |
| Configuración opcional | ✅ Ideal |
| APIs públicas | ✅ Muy útil |
| CRUD simple interno | ⚠️ Puede ser overkill |
Conclusión
El patrón Maybe/Option transforma valores opcionales en ciudadanos de primera clase, haciendo el código más seguro, expresivo y fácil de razonar.