Functional programming con TypeScript: lo que fp-ts enseña aunque no lo uses en producción
La solución correcta para manejar nullables en TypeScript es agregar más tipos. Sé que suena a burocracia. Pero fue justamente la idea detrás de Option<T> de fp-ts lo que me hizo ver que cada undefined que devolvía sin contexto era un contrato roto esperando explotar en runtime.
No instalé fp-ts en producción. No lo necesité. Pero leer su source me cambió cómo pienso los flujos de datos en TypeScript estricto, en Server Actions, en Zod schemas, en cualquier función que merezca una firma honesta.
Mi tesis es esta, y la sostengo con matices: fp-ts es una universidad, no un framework que la mayoría debería deployar. Pero hay una segunda parte que casi nadie dice en voz alta: los tres conceptos que valen la pena (Option, Either, pipe) se pueden internalizar en un TypeScript vanilla sin pagar el costo de onboarding del ecosistema completo. La librería no es el punto. El vocabulario mental que te deja sí lo es.
Por qué fp-ts incomoda a los desarrolladores de TypeScript del mundo real
El problema no es que fp-ts sea difícil. El problema es que impone un vocabulario completo —Functor, Monad, TaskEither, IO— antes de que puedas hacer algo tan básico como parsear una fecha sin explotar.
Si venís de un stack pragmático —Next.js, Prisma, Zod, Railway— la curva inicial parece costosa sin retorno claro. Y en muchos casos ese escepticismo es justo. El overhead filosófico existe.
Pero hay tres conceptos dentro de fp-ts que sobreviven fuera del ecosistema funcional puro: Option, Either y pipe. Son ideas, no solo librerías. Y esa distinción importa.
Option, Either y pipe: lo que se lleva quien no instala nada
Option: hacé explícito lo que puede no estar
Option<A> es básicamente Some(value) | None. La idea: una función que puede no retornar un valor lo dice en su firma, no en la documentación.
En TypeScript vanilla, esto se traduce en un patrón que ya probablemente usás pero sin formalizar:
// Sin Option: el contrato está oculto en el tipo de retorno
function encontrarUsuario(id: string): Usuario | undefined {
return db.find(u => u.id === id)
}
// Con la idea de Option internalizada: el nombre y la estructura
// comunican que el resultado puede no existir
type Option<A> = { _tag: 'Some'; value: A } | { _tag: 'None' }
function encontrarUsuario(id: string): Option<Usuario> {
const usuario = db.find(u => u.id === id)
return usuario ? { _tag: 'Some', value: usuario } : { _tag: 'None' }
}
// El consumidor no puede ignorar el None sin un match explícito
function procesarUsuario(opt: Option<Usuario>): string {
if (opt._tag === 'None') return 'Usuario no encontrado'
return opt.value.nombre
}¿Necesitás importar fp-ts para esto? No. ¿El concepto te fuerza a pensar diferente? Sí.
En Server Actions de Next.js, donde el resultado de una operación de base de datos puede ser vacío por razones legítimas, este patrón previene que un undefined silencioso llegue al cliente sin que nadie lo maneje. Lo mismo aplica cuando combinás esto con Zod para validación en runtime: el schema falla explícitamente, no devuelve un nullable escondido.
Either: errores como valores, no como excepciones
Either<E, A> es Left(error) | Right(value). La convención es que Left carga el error y Right el resultado feliz.
El insight que se lleva: cuando una función puede fallar de maneras distintas, modelar eso en el tipo de retorno es más honesto que lanzar una excepción y esperar que alguien la atrape.
// Modelado de error con Either sin instalar fp-ts
type Either<E, A> =
| { _tag: 'Left'; error: E }
| { _tag: 'Right'; value: A }
type ErrorParseo = { tipo: 'formato_invalido'; mensaje: string }
type ErrorDB = { tipo: 'no_encontrado'; id: string }
type ErrorNegocio = ErrorParseo | ErrorDB
// El que llama sabe exactamente qué puede salir mal
async function obtenerPerfil(
rawId: unknown
): Promise<Either<ErrorNegocio, Perfil>> {
// Validación: puede fallar con ErrorParseo
if (typeof rawId !== 'string' || rawId.length === 0) {
return {
_tag: 'Left',
error: { tipo: 'formato_invalido', mensaje: 'ID debe ser string no vacío' }
}
}
// Consulta: puede fallar con ErrorDB
const perfil = await db.perfiles.findUnique({ where: { id: rawId } })
if (!perfil) {
return { _tag: 'Left', error: { tipo: 'no_encontrado', id: rawId } }
}
return { _tag: 'Right', value: perfil }
}Esto no es código académico. Es un patrón que aparece naturalmente cuando usás TypeScript strict y te cansás de try/catch anidados donde el tipo del error es unknown. Si además estás manejando caching en Next.js App Router, tener los errores como valores hace que las decisiones de revalidación sean mucho más predecibles.
pipe: composición sin anidamiento
pipe de fp-ts es una función de composición izquierda-derecha. El valor entra por la izquierda, las transformaciones se aplican en orden, el resultado sale por la derecha.
La idea en TypeScript sin dependencias externas:
// Sin pipe: anidamiento que se lee de adentro hacia afuera
const resultado = formatearFecha(filtrarActivos(ordenarPorNombre(usuarios)))
// Con pipe nativo (disponible en algunos entornos modernos)
// o implementación mínima propia:
function pipe<A>(value: A): A
function pipe<A, B>(value: A, fn1: (a: A) => B): B
function pipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C
function pipe(value: unknown, ...fns: Array<(x: unknown) => unknown>): unknown {
return fns.reduce((acc, fn) => fn(acc), value)
}
// Ahora se lee de izquierda a derecha, como pensás el flujo
const resultado = pipe(
usuarios,
ordenarPorNombre, // primero ordenás
filtrarActivos, // después filtrás
formatearFecha // después formateás
)El overhead de implementar pipe vos mismo es minimal. El beneficio en legibilidad cuando las transformaciones se encadenan es inmediato.
Dónde fp-ts cobra el overhead que no querés pagar
Hasta acá describí los conceptos que sobreviven desacoplados. Pero sería deshonesto no nombrar lo que hace que fp-ts sea inviable como framework de producción para la mayoría de los equipos:
El ecosistema completo exige commit total. TaskEither, ReaderTaskEither, IOEither son abstracciones poderosas pero el código resultante es difícil de leer para alguien que no vive en ese paradigma. En un equipo de tres personas con niveles variados de TypeScript, agregar fp-ts como dependencia de producción introduce una barrera cognitiva real.
El tipado es verboso de una manera que TypeScript nativo ya resuelve mejor en 2025. Con satisfies, as const, tipos discriminados y el operador infer, TypeScript moderno cubre mucho del territorio que fp-ts llenaba cuando los tipos eran menos expresivos.
No hay escapatoria a la ley de todo o nada. Si usás pipe y Option de fp-ts junto con código imperativo mezclado, el resultado es peor que elegir uno de los dos. La consistencia es costosa.
Esto no es una crítica a fp-ts —el repositorio oficial tiene una ingeniería notable y Giulio Canti construyó algo serio. Es una observación sobre fit: la mayoría de los proyectos no tiene el contexto de equipo ni la codebase homogénea para absorber el costo.
Checklist: cuándo internalizar los conceptos vs. instalar la librería
Antes de decidir qué hacer con fp-ts en un proyecto real, pasá por esta matriz:
| Criterio | Internalizar conceptos | Instalar fp-ts |
|---|---|---|
| Equipo de 1-2 personas con TypeScript estricto | ✅ | ⚠️ Evaluar |
| Equipo mixto, distintos niveles de TS | ✅ | ❌ |
| Codebase nueva, greenfield | ✅ | ⚠️ Solo si el equipo ya conoce FP |
| Proyecto con muchos flujos async/error complejos | ✅ | ✅ Si el equipo está alineado |
| Librería que van a publicar (no app) | ✅ | ❌ No agregues la dep a otros |
| Querés aprender FP en TypeScript | ✅ | ✅ Para estudio, no prod inmediata |
Qué mirar antes de instalar cualquier cosa:
- ¿El equipo puede leer
ReaderTaskEither<R, E, A>sin googlear? - ¿Hay un linter configurado que valide que el estilo fp es consistente?
- ¿Los errores del dominio ya están tipados en discriminated unions?
- ¿Usás
strict: trueentsconfig.json? (Si no, empezá por ahí; el post sobre strict mode en TypeScript cubre las 6 opciones que más importan.)
Si respondiste no a las primeras tres, los conceptos de fp-ts te sirven más como guía de diseño que como dependencia activa.
Qué NO podés concluir de este análisis
Estos son los límites honestos de lo que este post puede afirmar:
- No hay benchmarks de performance entre fp-ts y TypeScript nativo. Si eso es crítico para tu decisión, necesitás medirlo en el contexto propio.
- No hay datos de adopción en equipos reales que digan cuánto tiempo tarda un equipo promedio en absorber fp-ts. Las anécdotas en Twitter van en ambas direcciones.
- La playlist de Sahand Javid (Functional Programming with TypeScript) es un buen punto de entrada y evidencia de interés en la comunidad, pero no es documentación oficial de casos de éxito en producción.
- Lo que funciona en un Server Action de Next.js puede no ser el patrón correcto para un servicio de background jobs con concurrencia alta. El contexto cambia las ecuaciones.
Errores comunes al acercarse a fp-ts
Leer la documentación teórica antes del código. El repositorio de fp-ts es denso en teoría de categorías. Si arrancás por ahí sin ver código concreto, es probable que abandones. Mejor empezar por los ejemplos de Option y Either directamente.
Convertir todo el código imperativo existente. El peor resultado posible es una codebase mitad funcional, mitad imperativa sin consistencia. Si vas a adoptar el estilo, necesitás un límite claro: módulo nuevo, feature nueva, no refactor mezclado.
Confundir pipe con composición total. pipe mejora la legibilidad de transformaciones lineales. No resuelve la complejidad de flujos bifurcados o con efectos secundarios. Para eso necesitás Either o TaskEither, que tiene su propio costo cognitivo.
Asumir que fp-ts es el único camino al código funcional en TypeScript. Esto es lo que más me sacaba de las casillas cuando lo veía repetido sin cuestionar: no hay una sola vía. Si lo que buscás es evitar mutaciones, preferir funciones puras y expresar errores en tipos, hay un argumento razonable —no una certeza absoluta— de que TypeScript vanilla con discriminated unions y un estilo de código consistente te acerca bastante a ese resultado, sin instalar nada. No tengo una medición exacta de cuánto de ese "80%" es real en cada codebase; depende del tamaño de los flujos de error y de cuánta disciplina tenga el equipo. Que lo adoptes o no es una decisión de equipo, no una verdad técnica cerrada.
FAQ: fp-ts y TypeScript funcional
¿Necesito saber Haskell para entender fp-ts?
No. Ayuda tener una intuición sobre tipos paramétricos y funciones de orden superior, pero no necesitás vocabulario de teoría de categorías para usar Option y Either productivamente. La playlist de Sahand Javid está pensada para desarrolladores TypeScript sin background funcional formal.
¿fp-ts está muerto? Vi que el desarrollo bajó el ritmo.
El repositorio fp-ts en GitHub sigue activo aunque el núcleo está estable. Giulio Canti está trabajando en effect, que es la evolución del ecosistema con un approach más pragmático y mejor integración con TypeScript moderno. Si estás evaluando el ecosistema en 2025, effect merece una mirada separada.
¿Se puede usar solo pipe de fp-ts sin traer todo el ecosistema?
Técnicamente sí, pero el tree-shaking de fp-ts no es perfecto. En la práctica, muchos equipos implementan su propio pipe de 10 líneas para evitar la dependencia. No hay nada mágico en la implementación de fp-ts que no puedas reproducir.
¿Cómo se integra esto con Zod?
Los schemas de Zod ya expresan el resultado de parsing como SafeParseReturnType<T> que es estructuralmente similar a Either. Si ya usás safeParse en lugar de parse, estás aplicando el mismo principio: errores como valores, no como excepciones. La integración con Zod en validación de runtime es natural si pensás en los schemas como funciones puras que retornan Either implícito.
¿Qué pasa con el debugging? ¿El stack trace con fp-ts es usable?
Este es uno de los costos reales que pocas guías mencionan. Cuando algo falla dentro de una cadena de pipe con varios map y chain, el stack trace puede ser confuso porque las funciones son anónimas o altamente genéricas. En desarrollo se resuelve con logging explícito entre pasos; en producción es un costo de operación que hay que tener en cuenta.
¿Aplica todo esto si trabajo con Server Actions en Next.js?
Específicamente Either es muy útil en Server Actions porque esas funciones pueden fallar de maneras distintas —validación, DB, permisos— y necesitás comunicar eso al cliente sin lanzar excepciones que Next.js captura de maneras que a veces no controlás. Modelar el retorno como un objeto discriminado es más predecible que depender del boundary de error de React.
Lo que haría diferente (y la postura que me quedo)
Si pudiera repetir el recorrido: leería el código fuente de fp-ts antes que cualquier tutorial. El source de Option y Either son cortos, bien tipados y más instructivos que diez artículos de blog incluyendo este.
Lo que me quedo de fp-ts no es la librería. Es el hábito de pensar en funciones como contratos donde la firma dice todo: qué entra, qué puede salir, qué puede fallar. Eso se aplica en cualquier TypeScript estricto, con o sin fp-ts instalado. Lo mismo aplica cuando diseñás tools portables para MCP o cualquier sistema donde el contrato de datos es la primera línea de defensa.
Lo que no compro es el evangelismo de que fp-ts es el único camino serio al TypeScript maduro. Es una herramienta con un fit muy específico. Fuera de ese fit, el overhead cognitivo y de onboarding supera los beneficios para la mayoría de los equipos en la mayoría de los contextos. Y lo incómodo de decir esto en voz alta es que buena parte del contenido sobre fp-ts en español no distingue entre "esto te enseña a pensar mejor" y "esto tenés que deployar". Son dos afirmaciones distintas y mezclarlas es lo que genera el rechazo automático en equipos pragmáticos.
Mi recomendación práctica: pasá una tarde con el repositorio oficial, implementá Option y Either por tu cuenta desde cero sin instalar nada, y decidí desde ahí si el ecosistema completo vale el costo para tu contexto. Si la implementación manual ya te resuelve el problema, ya tenés la respuesta. Y si después de esa tarde seguís pensando que necesitás TaskEither con todo el aparato detrás, probablemente tenés razón —pero llegaste ahí por evidencia propia, no por evangelismo ajeno.
Fuentes originales:
- fp-ts — GitHub oficial: https://github.com/gcanti/fp-ts
- Sahand Javid — Functional Programming with TypeScript (YouTube playlist): https://www.youtube.com/playlist?list=PLuPevXgCPUIMbCxBEnc1dNwboH6e2ImQo
Artículos Relacionados
Qwen3 en local con Ollama: qué cambió en la arquitectura y si vale el cambio
Qwen3 llegó con thinking mode y mejoras reales en código. Pero antes de reemplazar el modelo que ya tenés corriendo en Ollama, hay preguntas técnicas que responder primero. Acá las respondo sin vender hype.
02 ago 2026 · 9′ · Tutoriales · TypeScript · Inferencia Local
Prisma Query Logging y PostgreSQL: dónde termina el ORM y empieza la base
El log de queries de Prisma te muestra qué SQL generó el ORM, no por qué ese SQL tarda. Guía práctica para saber cuándo el logging alcanza y cuándo hay que entrar a PostgreSQL directamente.
29 jul 2026 · 8′ · Tutoriales · TypeScript · postgresql
Dependencias npm: cómo evaluar una librería antes de meterla en producción
Un package.json prolijo no dice nada sobre quién va a arreglar esa librería cuando se rompa. Criterio práctico para evaluar mantenimiento, superficie, tipos y transitive deps antes de sumar una dependencia a un stack TypeScript.
28 jul 2026 · 8′ · Tutoriales · TypeScript · pnpm
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.
Todavía nadie comentó. Sé el primero — tu opinión vale oro cuando somos pocos.