TypeScript

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ícate

Definició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:

  1. Determinismo: Para los mismos argumentos de entrada, siempre devuelve el mismo resultado, sin importar cuándo o dónde se ejecute.
  2. 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() o Date.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.

Aprende TypeScript online

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

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

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

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

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.