La semana pasada escribí sobre functional programming con TypeScript y lo que fp-ts enseña y llegué a una conclusión que me dejó cómodo pero no del todo tranquilo: un union type nativo con un par de funciones helper resuelve el ochenta por ciento de los casos donde alguien mete Either. El comentario que más recibí fue una variante de "entonces fp-ts no sirve para nada". Y ahí me di cuenta de que había dejado la puerta mal cerrada.
Mi tesis es esta: fp-ts no es una librería de "manejo de errores", es una librería para componer errores que vienen de fuentes distintas, en cantidad, sin que el código se convierta en una pirámide de ifs anidados. Si tu problema no tiene esa forma, la librería te agrega vocabulario sin agregarte claridad, y en ese caso estás pagando un peaje que no corresponde.
fp-ts alternativas typescript: el criterio que uso primero
Antes de meter fp-ts en un proyecto me hago una sola pregunta: ¿cuántos puntos de falla independientes tengo que combinar en una sola operación? Si la respuesta es uno o dos, un union type alcanza y no discuto más. Si la respuesta es "cinco validaciones que pueden fallar cada una por su cuenta y necesito acumular todos los errores, no solo el primero", ahí Either y sus combinadores empiezan a pagar su costo de aprendizaje.
Esa pregunta separa dos mundos. En el primero, alguien lee el código una vez y entiende el flujo. En el segundo, sin una abstracción que componga, terminás con un anidamiento de validaciones que nadie quiere tocar seis meses después — y lo digo porque ese código de "nadie quiere tocar" es el que después te toca a vos arreglar un viernes a las seis.
Qué dice la fuente oficial y qué no dice
El repo de fp-ts en GitHub se presenta como una librería de "typed functional programming in TypeScript", con implementaciones de estructuras como Option, Either, TaskEither y utilidades de composición como pipe y las instancias de Monad, Applicative y Functor para cada uno de esos tipos.
Lo que la documentación no dice — porque no es su trabajo decirlo — es cuándo conviene usarla en un proyecto real. Eso es una decisión de equipo, no una propiedad de la librería. La fuente te da la herramienta y el contrato tipado; no te dice si tu problema la necesita. Esa parte queda para quien diseña el sistema, y ahí es donde la mayoría de las discusiones que veo se quedan trabadas: la gente debate sintaxis cuando debería estar debatiendo si el problema tiene la forma que la herramienta resuelve.
Dónde se equivoca la gente: la receta que veo repetida
La receta común es: alguien lee sobre Either, le gusta la seguridad de tipos, y lo instala como default en cualquier función que pueda fallar. Un parseInt que puede devolver NaN. Una consulta a la base que puede no encontrar una fila. Un fetch que puede tirar 404. Todo envuelto en Either<Error, T>, con pipe, chain y fold en cada punto de la cadena.
El costo oculto no aparece en el archivo que escribe esa persona. Aparece cuando otro miembro del equipo — uno que no vive en el paradigma funcional todos los días — tiene que leer esa cadena para arreglar un bug. Tiene que entender qué es un Functor, por qué chain no es lo mismo que map, y por qué el error queda "atrapado" hasta que alguien lo desenvuelve con fold. Ese costo de lectura es real y no lo compensa la seguridad de tipos si el problema de fondo era simple. Lo incómodo de decir esto en voz alta es que a mí también me pasó: instalé Either en un lugar donde un if bastaba, solo porque lo había leído la semana anterior y quería usarlo.
El contraejemplo que sí justifica la inversión es distinto: un formulario con quince campos, cada uno con su propia validación, donde el resultado que necesitás mostrar al usuario es la lista completa de errores, no solo el primero que falló. Ahí Either combinado con Applicative — que permite acumular en vez de cortar en el primer error — resuelve algo que un union type nativo no resuelve sin reinventar la rueda a mano.
// pipeline de validacion compuesta, el caso donde fp-ts paga su costo
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
const validarEmail = (email: string): E.Either<string, string> =>
email.includes("@") ? E.right(email) : E.left("email invalido")
const validarEdad = (edad: number): E.Either<string, number> =>
edad >= 18 ? E.right(edad) : E.left("edad insuficiente")
// sequenceT o Apply permiten acumular errores de ambas validaciones
// en vez de cortar apenas la primera falla
Matriz de decisión: cuándo sí, cuándo no
Esto no es una tabla de verdades absolutas. Es el criterio que aplico antes de elegir, y depende del equipo que tenga que mantener el código después.
- Usalo si: hay que combinar tres o más validaciones independientes y necesitás el conjunto completo de errores, no el primero.
- Usalo si: el equipo ya tiene experiencia previa con programación funcional y el vocabulario no es una barrera de entrada.
-
Evitalo si: el flujo tiene un solo punto de falla que se puede resolver con un
ifo un union type de dos o tres variantes. - Evitalo si: el proyecto es de vida corta o el equipo rota seguido — la curva de aprendizaje no se amortiza.
- Mirá primero: cuántas personas del equipo van a tocar ese archivo en los próximos meses. Es la pregunta que más pesa en mi decisión real.
Esta misma lógica de "la abstracción paga cuando el problema tiene forma compuesta" la aplico en otros lados. Cuando escribí sobre revalidatePath vs revalidateTag en Next.js, el punto era parecido: la herramienta más fina gana cuando el caso de uso tiene granularidad real, y pierde cuando la fuerza bruta ya alcanza.
Los límites de esta comparación
No tengo un experimento propio con métricas de tiempo de onboarding entre equipos que usan fp-ts y equipos que no — si alguna vez lo corro, lo publico con números. No hay benchmark de performance entre Either y un union type que valga citar: para el caso típico, la diferencia de runtime es irrelevante porque ambos son estructuras livianas sin overhead real. Lo que tengo es un criterio de legibilidad basado en la forma del problema, no una medición productiva, y prefiero decirlo así en vez de disfrazarlo de dato.
Tampoco voy a afirmar que fp-ts sea "mejor" o "peor" en términos absolutos: depende del equipo, de la rotación de personas y de cuánta experiencia previa tenga el grupo con programación funcional. Si alguien busca una respuesta que no dependa del contexto, no la va a encontrar acá — y yo directamente desconfiaría de cualquier post que la ofrezca sin mostrar de dónde saca el número.
FAQ
¿fp-ts sigue siendo útil en 2025 o quedó reemplazado por TypeScript nativo?
Sigue siendo útil para el caso específico de composición de errores múltiples. TypeScript nativo con union types cubre la mayoría de los casos simples, pero no reemplaza los combinadores de acumulación que fp-ts ya trae resueltos.
¿Cuál es la alternativa más simple a Either de fp-ts?
Un union type del estilo { ok: true, value: T } | { ok: false, error: E }, combinado con funciones helper propias para map y chain si hacen falta. Cubre validaciones simples sin el vocabulario adicional.
¿fp-ts tiene mejor performance que manejar errores con try/catch?
No hay evidencia pública de una diferencia de performance relevante entre ambos enfoques para el caso típico de una aplicación. La decisión debería basarse en legibilidad y forma del problema, no en velocidad.
¿Vale la pena aprender fp-ts si nunca usé programación funcional?
Depende del proyecto. Si el equipo no tiene esa base y el problema no exige composición de errores múltiples, la curva de aprendizaje probablemente no se amortiza a tiempo.
¿Qué reemplaza a Option de fp-ts?
Un tipo T | null o T | undefined con las funciones de narrowing que TypeScript ya ofrece. Para el caso de "puede haber o no un valor", el lenguaje nativo alcanza casi siempre.
¿En qué proyectos SÍ recomendarías fp-ts desde el día uno?
En pipelines de validación de formularios o datos de entrada con múltiples reglas independientes, donde necesitás mostrar todos los errores encontrados y el equipo ya conoce el paradigma.
Mi postura final
No voy a recomendar fp-ts como default en un proyecto nuevo. Punto. Lo voy a recomendar cuando el problema tenga la forma que la librería resuelve mejor que cualquier otra cosa: validación compuesta con acumulación de errores. Fuera de ese caso, un union type nativo es más legible para el próximo que abra el archivo, y esa persona casi nunca es la misma que lo escribió — a veces ni siquiera es la misma versión de vos, seis meses después.
Si estás evaluando esta decisión en un proyecto real, el ejercicio concreto es simple: contá cuántos puntos de falla independientes tiene la función que estás escribiendo. Uno o dos, seguí con lo nativo, sin culpa. Tres o más con necesidad de acumular, ahí abrí la puerta a fp-ts — y aceptá de una que el equipo va a tardar en acostumbrarse, porque esa parte no se salta.
Fuente original:
- fp-ts GitHub: https://github.com/gcanti/fp-ts
Este artículo fue publicado originalmente en juanchi.dev