TypeScript
Tutorial TypeScript: Funciones puras y efectos secundarios
Aprende las características, identificación y refactorización de funciones puras y efectos secundarios en TypeScript para código más predecible y mantenible.
Aprende TypeScript y certifícateDefinición de funciones puras y su importancia
Una función pura es un concepto fundamental en programación funcional que cumple dos reglas básicas:
- Determinismo: Para los mismos argumentos de entrada, siempre devuelve el mismo resultado, sin importar cuándo o dónde se ejecute.
- Sin efectos secundarios: No modifica ningún estado fuera de su ámbito, ni depende de estados externos que puedan cambiar.
Una manera sencilla de entenderlo: una función pura sólo depende de sus parámetros de entrada para calcular su resultado y no modifica nada fuera de sí misma.
// Ejemplo de función pura
function add(a: number, b: number): number {
return a + b;
}
// Siempre devuelve 8 para los mismos parámetros
console.log(add(5, 3)); // 8
console.log(add(5, 3)); // 8 (siempre el mismo resultado)
Características de las funciones puras
Las funciones puras tienen tres propiedades esenciales:
// 1. Determinismo: mismas entradas => mismo resultado
function calculateCircleArea(radius: number): number {
return Math.PI * radius * radius;
}
// 2. Sin efectos secundarios: no modifica variables externas
function formatName(firstName: string, lastName: string): string {
return `${firstName} ${lastName}`;
}
// 3. Transparencia referencial: podemos sustituir la llamada por su resultado
const radius = 5;
const area = calculateCircleArea(radius); // Podríamos reemplazar esto por el valor 78.54...
Relación con la inmutabilidad
Las funciones puras están estrechamente relacionadas con la inmutabilidad que estudiamos en la lección anterior. En lugar de modificar datos existentes, crean y devuelven nuevos valores
// Función pura que trabaja con arrays de forma inmutable
function addItem<T>(array: T[], item: T): T[] {
// Creamos un nuevo array en lugar de modificar el original con push()
return [...array, item];
}
const numbers = [1, 2, 3];
const newNumbers = addItem(numbers, 4);
console.log(numbers); // [1, 2, 3] - el original no cambia
console.log(newNumbers); // [1, 2, 3, 4] - nuevo array con el elemento añadido
Beneficios de las funciones puras
Las funciones puras ofrecen ventajas significativas que mejoran la calidad del código:
- Facilidad de prueba: Al no depender del contexto externo, son más fáciles de probar unitariamente.
// Fácil de probar: solo necesitamos verificar que 2+3=5
test('add function correctly adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
- Predictibilidad: Su comportamiento es consistente y sin sorpresas.
- Seguridad para concurrencia: Al no compartir estado mutable, son seguras en entornos multihilo.
- Razonamiento simplificado: Podemos entender una función analizando solo su código, sin preocuparnos por el estado global.
- Componibilidad: Las funciones puras se pueden combinar fácilmente para crear funcionalidades más complejas.
// Podemos combinar funciones puras fácilmente
const double = (x: number): number => x * 2;
const increment = (x: number): number => x + 1;
// Componemos las funciones
const doubleAndIncrement = (x: number): number => increment(double(x));
console.log(doubleAndIncrement(3)); // 7 (primero duplica: 6, luego incrementa: 7)
Identificación y manejo de efectos secundarios
¿Que son los efectos secundarios?
Los efectos secundarios son cambios observables que ocurren fuera del ámbito de una función. Una función tiene efectos secundarios cuando:
// Modifica variables globales
let counter = 0;
function incrementCounter(): number {
counter++; // Efecto secundario: modifica una variable externa
return counter;
}
// Interactúa con el sistema de archivos, red o APIs
function fetchUserData(userId: string): Promise<any> {
return fetch(`https://api.example.com/users/${userId}`); // Efecto secundario: llamada a API
}
// Modifica el DOM o la interfaz de usuario
function updateUI(message: string): void {
document.getElementById('status')!.textContent = message; // Efecto secundario: modifica el DOM
}
// Interactúa con servicios del sistema
function saveToLocalStorage(key: string, data: any): void {
localStorage.setItem(key, JSON.stringify(data)); // Efecto secundario: modifica el almacenamiento
}
Tipos comunes de efectos secundarios
Los efectos secundarios más frecuentes en aplicaciones TypeScript incluyen:
- Modificación de variables fuera del ámbito de la función
- Operaciones de entrada/salida (E/S)
- Manipulación del DOM o la interfaz de usuario
- Llamadas a API o servicios externos
- Uso de funciones no deterministas (como
Math.random()
oDate.now()
)
Cómo identificar efectos secundarios
Existen señales que indican la presencia de efectos secundarios en el código:
// 1. Funciones que no retornan valores (tipo void)
function logMessage(message: string): void {
console.log(message); // Efecto secundario: escritura en la consola
}
// 2. Acceso a variables fuera del ámbito de la función
let globalConfig = { apiUrl: 'https://api.example.com' };
function getApiData(): Promise<any> {
return fetch(globalConfig.apiUrl); // Efecto secundario: depende de estado global
}
// 3. Nombres de funciones que sugieren acciones en lugar de transformaciones
// saveUser(), updateStatus(), deleteRecord(), sendEmail() suelen indicar efectos secundarios
Efectos secundarios necesarios
No todos los efectos secundarios son "malos" - muchos son necesarios para que nuestras aplicaciones sean útiles. Lo importante es:
- Identificarlos claramente
- Aislarlos del resto del código
- Manejarlos de forma controlada
// Ejemplo de aislamiento de efectos secundarios
// Separamos la lógica pura de los efectos secundarios
// Parte pura: cálculo del total
function calculateOrderTotal(items: Array<{price: number; quantity: number}>): number {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}
// Efecto secundario aislado: guardado en base de datos
function saveOrderToDatabase(order: Order): Promise<void> {
return database.orders.save(order);
}
// Función que orquesta ambas partes
async function processOrder(order: Order): Promise<Order> {
// Primero calculamos el total (operación pura)
const total = calculateOrderTotal(order.items);
// Creamos una nueva orden con el total (operación pura)
const completedOrder = { ...order, total };
// Finalmente realizamos el efecto secundario
await saveOrderToDatabase(completedOrder);
return completedOrder;
}
Técnicas para transformar funciones impuras en puras
Principio básico: separación de responsabilidades
La técnica fundamental es separar la lógica pura (cálculos, transformaciones) de los efectos secundarios (E/S, modificaciones de estado):
// Ejemplo de aislamiento de efectos secundarios
// Separamos la lógica pura de los efectos secundarios
// Parte pura: cálculo del total
function calculateOrderTotal(items: Array<{price: number; quantity: number}>): number {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
}
// Efecto secundario aislado: guardado en base de datos
function saveOrderToDatabase(order: Order): Promise<void> {
return database.orders.save(order);
}
// Función que orquesta ambas partes
async function processOrder(order: Order): Promise<Order> {
// Primero calculamos el total (operación pura)
const total = calculateOrderTotal(order.items);
// Creamos una nueva orden con el total (operación pura)
const completedOrder = { ...order, total };
// Finalmente realizamos el efecto secundario
await saveOrderToDatabase(completedOrder);
return completedOrder;
}
Uso de parámetros en lugar de estado global
Podemos transformar funciones impuras que dependen de estado global pasando ese estado como parámetros:
// Versión impura: depende de una variable global
let apiUrl = 'https://api.example.com';
function fetchData(endpoint: string): Promise<any> {
return fetch(`${apiUrl}/${endpoint}`);
}
// Versión pura: recibe la URL como parámetro
function fetchData(apiUrl: string, endpoint: string): Promise<any> {
return fetch(`${apiUrl}/${endpoint}`);
}
Inyección de dependencias
Para servicios externos o funcionalidades con efectos secundarios, podemos inyectarlos como parámetros:
// Versión impura: dependencia directa del servicio
function getUserData(userId: string): User {
// Dependencia directa de la implementación
return databaseService.getUser(userId);
}
// Versión mejorada: inyección de dependencias
interface UserRepository {
getUser(id: string): User;
}
function getUserData(userRepo: UserRepository, userId: string): User {
return userRepo.getUser(userId);
}
// Ahora podemos pasar diferentes implementaciones:
// - Implementación real para producción
// - Implementación mock para testing
Transformación de operaciones sobre arrays
Las operaciones mutables sobre arrays son comunes. Podemos transformarlas en versiones inmutables:
// Operaciones impuras sobre arrays
function addItemImpure(cart: string[]): void {
cart.push("New Item"); // Muta el array original
}
function removeItemImpure(cart: string[], index: number): void {
cart.splice(index, 1); // Muta el array original
}
// Versiones puras e inmutables
function addItem(cart: string[]): string[] {
return [...cart, "New Item"]; // Crea un nuevo array
}
function removeItem(cart: string[], index: number): string[] {
return [...cart.slice(0, index), ...cart.slice(index + 1)]; // Crea un nuevo array
}
// Versiones puras usando métodos funcionales
function filterExpensiveItems(products: Product[]): Product[] {
return products.filter(product => product.price > 100);
}
function applyDiscount(products: Product[]): Product[] {
return products.map(product => ({
...product,
price: product.price * 0.9 // 10% de descuento
}));
}
Patrones para objetos inmutables
Podemos aplicar actualizaciones inmutables a objetos para mantener la pureza:
// Operación impura sobre objeto
function updateUserAgeImpure(user: User): void {
user.age += 1; // Modifica el objeto original
}
// Versión pura con spread operator
function updateUserAge(user: User): User {
return { ...user, age: user.age + 1 }; // Crea un nuevo objeto
}
// Actualización de propiedades anidadas
function updateUserAddress(
user: User,
newCity: string
): User {
return {
...user,
address: {
...user.address,
city: newCity
}
};
}
3.6 Aplicando estas técnicas a un ejemplo completo
Veamos una refactorización completa de una función impura, aplicando los principios que hemos visto:
// Versión original: función impura con múltiples efectos secundarios
function processUserData(userId: string): void {
// Efecto secundario: obtiene datos del almacenamiento
const userData = localStorage.getItem(`user_${userId}`);
const user = userData ? JSON.parse(userData) : { id: userId, visits: 0 };
// Modifica el objeto directamente
user.visits++;
user.lastVisit = new Date().toISOString();
// Múltiples efectos secundarios
localStorage.setItem(`user_${userId}`, JSON.stringify(user));
document.getElementById('userName')!.textContent = user.name || 'Guest';
if (user.visits > 10) {
// Otro efecto secundario
sendAnalytics('frequent_user', userId);
}
}
// Versión refactorizada: separando la lógica pura de los efectos secundarios
interface User {
id: string;
name?: string;
visits: number;
lastVisit?: string;
}
// 1. Funciones puras
function createDefaultUser(userId: string): User {
return { id: userId, visits: 0 };
}
function updateUserStats(user: User): User {
return {
...user,
visits: user.visits + 1,
lastVisit: new Date().toISOString()
};
}
function isFrequentUser(user: User): boolean {
return user.visits > 10;
}
// 2. Funciones con efectos secundarios aislados
function getUserFromStorage(userId: string): User {
const userData = localStorage.getItem(`user_${userId}`);
return userData ? JSON.parse(userData) : createDefaultUser(userId);
}
function saveUserToStorage(user: User): void {
localStorage.setItem(`user_${user.id}`, JSON.stringify(user));
}
function updateUserUI(user: User): void {
document.getElementById('userName')!.textContent = user.name || 'Guest';
}
function trackUserAnalytics(userId: string, eventName: string): void {
sendAnalytics(eventName, userId);
}
// 3. Función orquestadora que compone las anteriores
function processUserData(userId: string): User {
// Obtenemos el usuario (efecto secundario)
const user = getUserFromStorage(userId);
// Actualizamos estadísticas (operación pura)
const updatedUser = updateUserStats(user);
// Guardamos los datos (efecto secundario)
saveUserToStorage(updatedUser);
// Actualizamos UI (efecto secundario)
updateUserUI(updatedUser);
// Comprobamos y enviamos analíticas si es necesario (efecto secundario condicional)
if (isFrequentUser(updatedUser)) {
trackUserAnalytics(userId, 'frequent_user');
}
return updatedUser;
}
En este ejemplo refactorizado:
- Cada función tiene una única responsabilidad
- Las funciones puras realizan cálculos y transformaciones
- Los efectos secundarios están aislados en funciones específicas
- La función principal orquesta todo el proceso
Conclusión
Las funciones puras son una herramienta poderosa que nos permite escribir código más predecible, testeable y mantenible. Al identificar y aislar los efectos secundarios, podemos razonar mejor sobre nuestro código y prevenir errores difíciles de detectar.
En la práctica, no todo nuestro código puede ser 100% puro, pero podemos aplicar estos principios para organizar nuestro código en componentes bien definidos donde la lógica pura está separada de los efectos secundarios necesarios.
La combinación de inmutabilidad (vista en la lección anterior) y funciones puras nos proporciona una base sólida para aplicar principios de programación funcional en nuestras aplicaciones TypeScript, resultando en sistemas más robustos y mantenibles.
En la próxima lección, profundizaremos en funciones de primera clase y orden superior, que son otro pilar importante de la programación funcional.
Otros ejercicios de programación de TypeScript
Evalúa tus conocimientos de esta lección Funciones puras y efectos secundarios 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 de funciones puras y sus características principales: determinismo, ausencia de efectos secundarios y transparencia referencial.
- Identificar correctamente diferentes tipos de efectos secundarios en código TypeScript y explicar por qué afectan a la pureza de las funciones.
- Reconocer los beneficios prácticos de utilizar funciones puras, como mejor testabilidad, predictibilidad y seguridad en entornos concurrentes.
- Aplicar técnicas básicas para transformar funciones impuras en versiones puras mediante la separación de responsabilidades.
- Implementar operaciones inmutables en arrays y objetos para mantener la pureza funcional, aplicando los conocimientos de inmutabilidad de la lección anterior.