TypeScript
Tutorial TypeScript: Composición de funciones
Aprende la composición de funciones en TypeScript para crear código modular y reutilizable con ejemplos prácticos de pipe y compose.
Aprende TypeScript y certifícateConcepto de composición funcional
La composición de funciones es uno de los pilares fundamentales de la programación funcional. Este concepto proviene directamente de las matemáticas, donde se define como la aplicación de una función al resultado de otra. En términos simples, es una técnica que nos permite combinar múltiples funciones para crear una nueva función más compleja.
Imagina que tienes dos funciones: una que convierte un texto a mayúsculas y otra que elimina espacios. La composición te permitiría crear una nueva función que realice ambas operaciones de manera secuencial, aplicando primero una y luego la otra al resultado.
¿Qué es la Composición Funcional?
La composición de funciones es una técnica que consiste en combinar dos o más funciones para crear una nueva función. El resultado de una función se convierte en la entrada de la siguiente, creando un flujo de datos continuo. Este concepto proviene directamente de las matemáticas, donde se define como la aplicación de una función al resultado de otra.
En matemáticas, la composición de funciones se representa como (f∘g)(x), que significa "f compuesta con g". Se evalúa como f(g(x)), es decir, primero se aplica g
a x
, y luego se aplica f
al resultado de g(x)
.
Ejemplo Básico (Composición Manual): Imagina que tienes dos funciones simples: una que duplica un número y otra que le suma uno.
// Dos funciones simples
const double = (x: number): number => x * 2;
const addOne = (x: number): number => x + 1;
// Composición manual
const doubleAndAddOne = (x: number): number => addOne(double(x));
console.log(doubleAndAddOne(3)); // Resultado: 7 (primero 3*2=6, luego 6+1=7)
En este ejemplo, doubleAndAddOne
es una nueva función que combina el comportamiento de double
y addOne
.
Ventajas de la composición funcional
La composición de funciones no es solo una curiosidad matemática; ofrece ventajas significativas en el desarrollo de software:
- Modularidad: Fomenta la creación de funciones pequeñas y especializadas que hacen una sola cosa bien. Esto hace que cada pieza de código sea más fácil de entender y probar de forma aislada.
- Reutilización: Estas funciones pequeñas y puras son como "bloques de construcción" que pueden combinarse de diferentes maneras para crear nuevas funcionalidades sin tener que reescribir código.
- Legibilidad: El código se vuelve más declarativo y fácil de razonar. En lugar de describir
cómo
se llega a un resultado (paso a paso), la composición expresaqué
se está haciendo (una secuencia de transformaciones). - Mantenibilidad: Es más fácil depurar y modificar funciones pequeñas y bien definidas que grandes bloques de código con lógica compleja.
Dirección de la Composición: compose
vs pipe
Un aspecto importante a entender es la dirección en la que fluyen los datos durante la composición.
compose
(Derecha a Izquierda): Sigue la notación matemática estándar (f∘g)(x), donde las funciones se aplican de derecha a izquierda (primerog
, luegof
).
// Implementación simple de 'compose' para dos funciones
const compose = <A, B, C>(f: (b: B) => C, g: (a: A) => B) =>
(x: A): C => f(g(x));
// Usando el ejemplo anterior
const doubleAndAddOneCompose = compose(addOne, double);
console.log(doubleAndAddOneCompose(3)); // 7 (double(3) -> 6, addOne(6) -> 7)
pipe
(Izquierda a Derecha): En programación, a menudo resulta más intuitivo leer el flujo de datos de izquierda a derecha, como una "tubería" o "pipeline". La funciónpipe
invierte el orden de composición para facilitar esta lectura.
// Implementación simple de 'pipe' para dos funciones
const pipe = <A, B, C>(g: (a: A) => B, f: (b: B) => C) =>
(x: A): C => f(g(x));
// Usando el ejemplo anterior
const doubleAndAddOnePipe = pipe(double, addOne);
console.log(doubleAndAddOnePipe(3)); // 7 (double(3) -> 6, addOne(6) -> 7)
Aunque para dos funciones la implementación se vea igual, la diferencia conceptual es importante para la legibilidad cuando componemos más funciones. La función pipe
es generalmente preferida en JavaScript/TypeScript por su fluidez de lectura con el flujo de datos.
Implementación Práctica de la Composición
Para construir composiciones más complejas, especialmente cuando necesitamos encadenar más de dos funciones, implementamos versiones más robustas de pipe
y compose
que aceptan un número variable de argumentos, a menudo utilizando el método reduce
de arrays.
Implementación de pipe
para Múltiples Funciones
La función pipe
es ideal para construir pipelines donde el resultado de una función se pasa a la siguiente. Utilizaremos Array.prototype.reduce()
para encadenar las funciones de izquierda a derecha.
/**
* Compone una secuencia de funciones de izquierda a derecha.
* El resultado de cada función se pasa como argumento a la siguiente.
* @param fns Un array de funciones a componer.
* Cada función (excepto la última) debe tener un tipo de retorno
* compatible con el tipo de argumento de la siguiente función.
* @returns Una nueva función que toma un valor inicial y lo pasa
* a través de todas las funciones en secuencia.
*/
function pipe<TArgs extends any[], R>(
...fns: {
[K in keyof TArgs]: K extends '0'
? (arg: TArgs[0]) => TArgs[1]
: K extends string
? (arg: TArgs[number]) => TArgs[number]
: never
} & { length: TArgs['length'] } & Array<(...args: any[]) => any>
): (initialValue: TArgs[0]) => R;
// Implementación concreta para casos comunes (simplificado para evitar tipos genéricos muy complejos)
// En la práctica, TypeScript puede inferir esto para la mayoría de los casos,
// o se usarían sobrecargas para una tipado más estricto en escenarios complejos.
function pipe<T>(...fns: Array<(arg: T) => T>): (value: T) => T {
return (value) => fns.reduce((acc, fn) => fn(acc), value);
}
// Ejemplo de uso con múltiples transformaciones de datos
const removeSpaces = (text: string): string => text.replace(/\s/g, '');
const toLowerCase = (text: string): string => text.toLowerCase();
const countChars = (text: string): number => text.length;
// Creamos un pipeline para procesar texto
const countCharsNoSpacesAndLowercase = pipe(
toLowerCase, // Primero convierte el texto a minúsculas
removeSpaces, // Luego elimina todos los espacios
countChars // Finalmente cuenta los caracteres restantes
);
const textInput = "Hello TypeScript World";
console.log(`Texto original: "${textInput}"`);
console.log(`Resultado: ${countCharsNoSpacesAndLowercase(textInput)}`); // Salida: 19 (todos en minúsculas, sin espacios)
// Otro ejemplo más complejo
const processTextForSearch = pipe(
removeSpaces,
toLowerCase,
(text: string) => text.replace(/[^a-z0-9]/g, ''), // Elimina caracteres no alfanuméricos
(text: string) => text.slice(0, 10) // Limita a 10 caracteres
);
const searchInput = "My Awesome Project Name!";
console.log(`Entrada búsqueda: "${searchInput}"`);
console.log(`Texto procesado para búsqueda: "${processTextForSearch(searchInput)}"`); // Salida: "myawesomep"
Nota sobre el tipado: La implementación de pipe
y compose
con tipos genéricos para manejar flujos de datos donde el tipo de retorno de una función es el tipo de entrada de la siguiente puede ser compleja. Para esta lección, nos centramos en la implementación común donde las funciones operan sobre el mismo tipo de datos ((arg: T) => T
). En TypeScript real, se pueden usar técnicas avanzadas de sobrecarga o bibliotecas que ya manejen este tipado complejo para usted.
Implementación de compose
para Múltiples Funciones
Similar a pipe
, podemos implementar una versión de compose
que acepte múltiples funciones. La diferencia clave es que compose
aplica las funciones de derecha a izquierda, lo que significa que la última función en el array se ejecuta primero. Esto se logra usando Array.prototype.reduceRight()
.
/**
* Compone una secuencia de funciones de derecha a izquierda.
* El resultado de cada función se pasa como argumento a la anterior.
* @param fns Un array de funciones a componer.
* @returns Una nueva función que toma un valor inicial y lo pasa
* a través de todas las funciones en secuencia (de derecha a izquierda).
*/
function compose<T>(...fns: Array<(arg: T) => T>): (value: T) => T {
return (value) => fns.reduceRight((acc, fn) => fn(acc), value);
}
// Ejemplo de uso
const doubleNum = (n: number): number => n * 2;
const squareNum = (n: number): number => n * n;
const addFiveNum = (n: number): number => n + 5;
// Composición de múltiples funciones (addFive(square(double(3))))
const complexCalculationCompose = compose(
addFiveNum, // Se aplica al final
squareNum, // Se aplica en segundo lugar
doubleNum // Se aplica primero
);
console.log(`Calculo complejo de 3 (compose): ${complexCalculationCompose(3)}`); // Salida: 41 (3 -> double(3)=6 -> square(6)=36 -> addFive(36)=41)
La elección entre pipe
y compose
es a menudo una cuestión de preferencia y legibilidad para el equipo. pipe
es más popular por su flujo de lectura de izquierda a derecha, que coincide con la forma en que leemos el código.
Patrones y Aplicaciones de la Composición
La composición funcional es una herramienta poderosa para organizar la lógica de nuestro programa, especialmente en el procesamiento de datos y la construcción de pipelines.
Aplicaciones Clave de la Composición (Flujo de Datos)
La composición brilla al construir flujos de procesamiento de datos, donde los datos se someten a una serie de transformaciones.
Transformaciones Secuenciales de Datos
Podemos crear funciones de procesamiento complejas a partir de otras más simples, haciendo el código muy declarativo.
// Funciones de transformación de usuario
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
const normalizeEmail = (user: User): User => ({
...user,
email: user.email.toLowerCase().trim()
});
const formatUsername = (user: User): User => ({
...user,
name: user.name.charAt(0).toUpperCase() + user.name.slice(1).toLowerCase()
});
const validateEmail = (user: User): User => {
if (!user.email.includes('@')) {
throw new Error(`Invalid email format for user ${user.id}`);
}
return user;
};
// Composición para procesar usuarios
const processUser = pipe(
normalizeEmail, // Limpiar y poner email en minúsculas
validateEmail, // Validar formato de email (puede lanzar error)
formatUsername // Poner primera letra del nombre en mayúscula y resto en minúsculas
);
// Uso
const rawUser = { id: 1, name: "jOHN doe", email: " JOHN.DOE@example.com ", isActive: true };
try {
const processedUser = processUser(rawUser);
console.log("Usuario procesado:", processedUser);
// Salida: { id: 1, name: "John doe", email: "john.doe@example.com", isActive: true }
} catch (error: any) {
console.error("Error al procesar usuario:", error.message);
}
const invalidUser = { id: 2, name: "jane", email: "invalid-email", isActive: false };
try {
processUser(invalidUser); // Esto lanzará un error
} catch (error: any) {
console.error("Error al procesar usuario (inválido):", error.message);
// Salida: Error al procesar usuario (inválido): Invalid email format for user 2
}
Procesamiento de Colecciones
La composición es extremadamente útil para construir pipelines de procesamiento de arrays, combinando los métodos funcionales de arrays que vimos en la lección anterior (map
, filter
, reduce
, sort
).
// Datos de ejemplo de inventario
interface Product {
id: number;
name: string;
price: number;
category: string;
stock: number;
}
const inventory: Product[] = [
{ id: 1, name: "Laptop", price: 1200, category: "electronics", stock: 15 },
{ id: 2, name: "Headphones", price: 80, category: "electronics", stock: 0 },
{ id: 3, name: "Coffee Maker", price: 120, category: "kitchen", stock: 5 },
{ id: 4, name: "Desk Chair", price: 250, category: "furniture", stock: 10 },
{ id: 5, name: "Smartphone", price: 800, category: "electronics", stock: 7 }
];
// Funciones de transformación para el pipeline
const filterInStock = (products: Product[]): Product[] =>
products.filter(p => p.stock > 0);
const filterByCategory = (category: string) =>
(products: Product[]): Product[] =>
products.filter(p => p.category === category);
const applyDiscount = (percentage: number) =>
(products: Product[]): Product[] =>
products.map(p => ({
...p,
price: parseFloat((p.price * (1 - percentage / 100)).toFixed(2)) // Redondear a 2 decimales
}));
const sortByPrice = (products: Product[]): Product[] =>
[...products].sort((a, b) => a.price - b.price);
// Creamos un pipeline para procesar productos:
// 1. Filtrar solo los productos en stock.
// 2. Filtrar solo los de la categoría "electronics".
// 3. Aplicar un 10% de descuento.
// 4. Ordenar por precio de menor a mayor.
const processElectronicsInventory = pipe(
filterInStock,
filterByCategory("electronics"),
applyDiscount(10),
sortByPrice
);
// Aplicamos el pipeline al inventario
const processedProducts = processElectronicsInventory(inventory);
console.log("Productos electrónicos procesados:", processedProducts);
/*
Salida esperada:
[
{ id: 5, name: 'Smartphone', price: 720, category: 'electronics', stock: 7 },
{ id: 1, name: 'Laptop', price: 1080, category: 'electronics', stock: 15 }
]
*/
Composición con Currying
El currying es una técnica de programación funcional que transforma una función con múltiples argumentos en una secuencia de funciones, cada una de las cuales toma un solo argumento. Las funciones currificadas son excelentes para la composición, ya que podemos crear versiones pre-configuradas de funciones que se ajustan perfectamente al pipeline.
// Función currificada para filtrar números mayores que un mínimo
const filterGreaterThan = (min: number) => (numbers: number[]): number[] =>
numbers.filter(n => n > min);
// Función currificada para multiplicar números por un factor
const multiplyBy = (factor: number) => (numbers: number[]): number[] =>
numbers.map(n => n * factor);
// Componemos funciones currificadas en un pipeline
const processNumbers = pipe(
filterGreaterThan(10), // Se aplica filterGreaterThan(10)
multiplyBy(2) // Luego se aplica multiplyBy(2)
);
const numbersToProcess = [5, 10, 15, 20, 25];
console.log(`Números originales: ${numbersToProcess}`);
console.log(`Números procesados (currying): ${processNumbers(numbersToProcess)}`); // Salida: [30, 40, 50]
Aquí, filterGreaterThan(10)
y multiplyBy(2)
devuelven funciones que esperan un array de números, que es exactamente lo que espera el pipe
.
Consideraciones y Patrones Avanzados (Moderados)
Manejo Básico de Errores en Pipelines
En aplicaciones reales, las funciones en un pipeline pueden fallar. Es importante tener un mecanismo básico para manejar errores. Podemos modificar nuestra función pipe
para envolver la ejecución de cada función en un bloque try-catch
.
// Implementación de 'pipe' con manejo básico de errores
function safePipe<T>(...fns: Array<(arg: T) => T>): (value: T) => T {
return (value) => {
try {
return fns.reduce((acc, fn) => fn(acc), value);
} catch (error) {
console.error("Error en el pipeline:", error);
return value; // Devolvemos el valor original o un valor por defecto en caso de error
}
};
}
// Ejemplo de uso con una función que puede lanzar un error
const parseJSON = (text: string): any => {
console.log(`Intentando parsear: "${text}"`);
return JSON.parse(text);
};
const getFirstItem = (array: any[]): any => array[0];
const safeGetFirstItem = safePipe(
parseJSON,
getFirstItem
);
console.log(`\n--- Manejo de Errores ---`);
console.log(`Resultado válido: ${safeGetFirstItem('[1,2,3]')}`); // Salida: Intentando parsear: "[1,2,3]" \n Resultado válido: 1
console.log(`Resultado con error: ${safeGetFirstItem('invalid json')}`); // Salida: Error en el pipeline: SyntaxError: Unexpected token 'i'... \n Resultado con error: invalid json
Este safePipe
es útil para pipelines donde un error en una etapa no debe detener todo el flujo, sino que permite una recuperación básica o el retorno del valor de entrada.
Comparación: Composición Funcional vs. Encadenamiento de Métodos (OOP)
Es importante distinguir la composición funcional del encadenamiento de métodos que a menudo vemos en la programación orientada a objetos (POO) (como array.map().filter().sort()
).
Fundamentos de cada enfoque:
- Composición Funcional: Se centra en combinar funciones puras e independientes para crear una nueva función. Los datos fluyen a través de las funciones. El estado (si existe) se pasa explícitamente como argumento.
- Encadenamiento de Métodos (OOP): Se basa en objetos que tienen métodos que, al ser llamados, devuelven el propio objeto (
this
) o una nueva instancia del objeto, permitiendo encadenar llamadas. Los métodos operan sobre el estado interno del objeto.
Diferencias Clave:
- Inmutabilidad vs. Estado Mutable: La composición funcional (con funciones puras) promueve la inmutabilidad; cada función produce un nuevo valor y el estado original no se modifica. El encadenamiento de métodos en OOP puede implicar la mutación del estado interno del objeto en cada paso (aunque muchas API modernas como los métodos de array de JS devuelven nuevas instancias).
- Flexibilidad y Reusabilidad: Las funciones en la composición son piezas de lógica desacopladas que pueden combinarse de infinitas maneras. Los métodos encadenados suelen estar acoplados a la clase o interfaz del objeto.
- Legibilidad: La composición (especialmente con
pipe
) puede mejorar la legibilidad al representar el flujo de datos. El encadenamiento de métodos es legible cuando las operaciones son intrínsecas al objeto.
Casos de Uso Apropiados:
- Composición Funcional: Ideal para construir flujos de procesamiento de datos complejos e independientes del estado. Excelente para la transformación de datos.
- Encadenamiento de Métodos: Adecuado cuando las operaciones están fuertemente ligadas al estado y comportamiento de un objeto específico, y cuando la secuencia de operaciones es inherente a las capacidades del objeto. (Ejemplo:
jQuery.select().addClass().hide()
).
Patrones Híbridos: En la práctica, es común combinar ambos enfoques. Por ejemplo, los métodos funcionales de arrays (map
, filter
, reduce
) se encadenan en sí mismos, pero también pueden ser usados como los "bloques de construcción" que se pasan a una función pipe
o compose
, creando así un pipeline de procesamiento de colecciones como vimos en el ejemplo anterior.
Rendimiento (Consejos)
Aunque la composición funcional hace que el código sea más legible y mantenible, un encadenamiento excesivo de muchas funciones muy pequeñas podría, en teoría, tener una ligera sobrecarga en comparación con un único bucle imperativo optimizado. Sin embargo, en la mayoría de las aplicaciones modernas y con los motores de JavaScript optimizados, esta diferencia es insignificante y el beneficio en la claridad del código supera con creces cualquier coste de rendimiento mínimo. La clave es la claridad y la mantenibilidad.
Conclusión:
La composición de funciones es una herramienta fundamental en la programación funcional que nos permite crear sistemas complejos a partir de componentes simples y reutilizables. Al dominar pipe
y compose
, y entender cómo se integran con otros conceptos como el currying y los métodos de array, podemos escribir código más modular, legible y robusto en TypeScript.
Otros ejercicios de programación de TypeScript
Evalúa tus conocimientos de esta lección Composición de funciones con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Funciones
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Proyecto Inventario de productos
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
Todas las lecciones de TypeScript
Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Typescript
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras Y Efectos Secundarios
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales Y Tipos Condicionales
Tipos Intermedios Y Avanzados
Tipos Genéricos Básicos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad (Partial, Required, Pick, Etc)
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el concepto fundamental de composición funcional y su origen en las matemáticas, donde una función se aplica al resultado de otra.
- Implementar funciones de composición básicas como
pipe
ycompose
para crear transformaciones de datos encadenadas. - Desarrollar pipelines de procesamiento de datos modulares, legibles y mantenibles usando técnicas de composición.
- Comparar la composición funcional con el encadenamiento de métodos, entendiendo cuándo usar cada enfoque.
- Aplicar la composición en casos prácticos como la transformación de datos, el procesamiento de colecciones y el manejo básico de errores.