Programación funcional en TypeScript: de la teoría a la práctica
La programación funcional (PF) es un paradigma en el que los cómputos se tratan como evaluación de funciones matemáticas y se evita el cambio de estado y los datos mutables. TypeScript, al ser una extensión de JavaScript, es excelente para aplicar los principios de PF gracias a su tipado estricto y soporte para funciones de orden superior. En este artículo analizaremos los conceptos clave de PF en TypeScript y mostraremos cómo aplicarlos en la práctica.
1. Funciones puras e inmutabilidad
Función pura es una función que:
- Siempre devuelve el mismo resultado para los mismos argumentos (determinismo).
- No tiene efectos secundarios (no modifica variables externas, no imprime datos, no realiza solicitudes).
Ejemplo de función pura:
// Función pura: suma dos númerosconst add = (a: number, b: number): number => a + b;
// Siempre devolverá 5console.log(add(2, 3)); // 5console.log(add(2, 3)); // 5Inmutabilidad es el principio por el cual los datos no se modifican después de su creación. En lugar de modificar un objeto existente, se crea uno nuevo.
// Actualización inmutable de un arrayconst numbers: number[] = [1, 2, 3];
// Malo (mutación):numbers.push(4);
// Bueno (inmutable):const newNumbers = [...numbers, 4]; // [1,2,3,4]
// Actualización inmutable de un objetointerface User { name: string; age: number;}
const user: User = { name: 'Alice', age: 30 };const updatedUser: User = { ...user, age: 31 }; // nuevo objeto2. Composición de funciones y currying
Composición de funciones es la combinación de dos o más funciones en una nueva. El resultado de la primera función se pasa como argumento a la segunda.
// Composición de dos funcionesconst compose = <T, U, V>(f: (x: U) => V, g: (x: T) => U) => (x: T): V => f(g(x));
const double = (x: number): number => x * 2;const increment = (x: number): number => x + 1;
const doubleThenIncrement = compose(increment, double);console.log(doubleThenIncrement(3)); // (3*2)+1 = 7Currying es la transformación de una función con múltiples argumentos en una secuencia de funciones con un solo argumento.
// Función normalconst multiply = (a: number, b: number): number => a * b;
// Versión currificadaconst curriedMultiply = (a: number) => (b: number): number => a * b;
const multiplyBy2 = curriedMultiply(2);console.log(multiplyBy2(5)); // 10console.log(multiplyBy2(7)); // 14El currying facilita la aplicación parcial y la reutilización de funciones.
3. Funtores y mónadas: Maybe y Either
Funtor es un contenedor que implementa el método map, permitiendo aplicar una función al valor dentro del contenedor. En TypeScript, un ejemplo de funtor es el array:
const numbers = [1, 2, 3];const doubled = numbers.map(x => x * 2); // [2, 4, 6]Mónada es un funtor que además implementa el método flatMap (o bind) y of (o return). Las mónadas ayudan a manejar efectos secundarios, valores null y errores sin excepciones.
Implementemos una mónada Maybe simple para trabajar de forma segura con null/undefined:
// Mónada Maybeclass Maybe<T> { private constructor(private value: T | null) {}
static of<T>(value: T): Maybe<T> { return new Maybe(value); }
static none<T>(): Maybe<T> { return new Maybe<T>(null); }
map<U>(fn: (value: T) => U): Maybe<U> { if (this.value === null || this.value === undefined) { return Maybe.none(); } return M