Métodos funcionales de arrays (map, filter, reduce)

Avanzado
TypeScript
TypeScript
Actualizado: 09/05/2025

¡Desbloquea el curso de TypeScript completo!

IA
Ejercicios
Certificado
Entrar

Mira la lección en vídeo

Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.

Desbloquear Plan Plus

Transformando datos con map()

El método map() es una de las herramientas más útiles para la manipulación de arrays en TypeScript. Este método nos permite transformar cada elemento de un array aplicando una función a cada uno de ellos, devolviendo un nuevo array con los resultados.

La característica más importante de map() es que no modifica el array original, siguiendo así el principio de inmutabilidad que es fundamental en la programación funcional. Esto nos permite trabajar con datos de forma más predecible y segura.

Sintaxis básica

La sintaxis de map() es bastante sencilla:

const nuevoArray = array.map(callback);

Donde el callback es una función que se ejecutará para cada elemento del array. Esta función recibe tres parámetros (aunque normalmente solo usamos el primero):

array.map((currentValue, index, array) => {
  // Lógica de transformación
  return valorTransformado;
});
  • currentValue: El elemento actual que está siendo procesado
  • index: La posición del elemento actual en el array
  • array: El array completo sobre el que se llamó map()

Ejemplos básicos

Veamos un ejemplo simple donde duplicamos cada número de un array:

const numbers: number[] = [1, 2, 3, 4, 5];
const doubled: number[] = numbers.map(num => num * 2);

console.log(doubled); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5] - El array original no se modifica

También podemos transformar tipos de datos. Por ejemplo, convertir números a strings:

const numbers: number[] = [1, 2, 3, 4, 5];
const stringNumbers: string[] = numbers.map(num => num.toString());

console.log(stringNumbers); // ["1", "2", "3", "4", "5"]

Tipado en TypeScript

Una de las ventajas de usar TypeScript es que podemos definir claramente los tipos de entrada y salida:

const numbers: number[] = [1, 2, 3, 4, 5];

// Explícitamente tipamos la función callback
const doubled: number[] = numbers.map((num: number): number => {
  return num * 2;
});

// TypeScript infiere automáticamente que stringNumbers es string[]
const stringNumbers = numbers.map(num => `Número: ${num}`);

Transformando objetos

Map() es especialmente útil cuando trabajamos con arrays de objetos, permitiéndonos extraer o transformar propiedades específicas:

interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: "Ana", email: "ana@example.com" },
  { id: 2, name: "Carlos", email: "carlos@example.com" },
  { id: 3, name: "Elena", email: "elena@example.com" }
];

// Extraer solo los nombres
const names: string[] = users.map(user => user.name);
console.log(names); // ["Ana", "Carlos", "Elena"]

// Crear nuevos objetos con formato diferente
interface UserDTO {
  userId: number;
  displayName: string;
}

const userDTOs: UserDTO[] = users.map(user => ({
  userId: user.id,
  displayName: user.name.toUpperCase()
}));

console.log(userDTOs);
// [
//   { userId: 1, displayName: "ANA" },
//   { userId: 2, displayName: "CARLOS" },
//   { userId: 3, displayName: "ELENA" }
// ]

Casos de uso prácticos

Formateo de datos para UI

Uno de los usos más comunes de map() es preparar datos para mostrarlos en una interfaz de usuario:

interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
}

const products: Product[] = [
  { id: 1, name: "Laptop", price: 999.99, stock: 5 },
  { id: 2, name: "Mouse", price: 25.50, stock: 0 },
  { id: 3, name: "Monitor", price: 349.99, stock: 3 }
];

// Transformar para mostrar en UI
const productListItems = products.map(product => ({
  id: product.id,
  name: product.name,
  formattedPrice: `$${product.price.toFixed(2)}`,
  isAvailable: product.stock > 0
}));

console.log(productListItems);
// [
//   { id: 1, name: "Laptop", formattedPrice: "$999.99", isAvailable: true },
//   { id: 2, name: "Mouse", formattedPrice: "$25.50", isAvailable: false },
//   { id: 3, name: "Monitor", formattedPrice: "$349.99", isAvailable: true }
// ]

Transformación de datos de API

Otro caso común es transformar datos recibidos de una API:

interface ApiUser {
  user_id: string;
  first_name: string;
  last_name: string;
  user_email: string;
}

interface AppUser {
  id: number;
  fullName: string;
  email: string;
}

const apiUsers: ApiUser[] = [
  { user_id: "1", first_name: "Juan", last_name: "Pérez", user_email: "juan@example.com" },
  { user_id: "2", first_name: "María", last_name: "López", user_email: "maria@example.com" }
];

// Transformar formato de API a formato de aplicación
const appUsers: AppUser[] = apiUsers.map(apiUser => ({
  id: parseInt(apiUser.user_id),
  fullName: `${apiUser.first_name} ${apiUser.last_name}`,
  email: apiUser.user_email
}));

console.log(appUsers);
// [
//   { id: 1, fullName: "Juan Pérez", email: "juan@example.com" },
//   { id: 2, fullName: "María López", email: "maria@example.com" }
// ]

Encadenando map() con otros métodos

Una práctica común en programación funcional es encadenar métodos para realizar transformaciones complejas:

interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: number;
}

const tasks: Task[] = [
  { id: 1, title: "Estudiar TypeScript", completed: false, priority: 2 },
  { id: 2, title: "Hacer ejercicio", completed: true, priority: 1 },
  { id: 3, title: "Comprar víveres", completed: false, priority: 3 },
  { id: 4, title: "Leer documentación", completed: false, priority: 2 }
];

// Obtener títulos de tareas pendientes de alta prioridad (prioridad > 1)
const highPriorityPendingTaskTitles = tasks
  .filter(task => !task.completed && task.priority > 1)
  .map(task => task.title);

console.log(highPriorityPendingTaskTitles);
// ["Estudiar TypeScript", "Comprar víveres"]

Consideraciones de rendimiento

Aunque map() es muy útil, es importante tener en cuenta algunas consideraciones de rendimiento:

  • Para arrays muy grandes, el rendimiento puede ser un problema si la función de transformación es compleja.
  • Si solo necesitas transformar algunos elementos (no todos), considera usar filter() antes de map().
  • Si necesitas acumular un resultado mientras transformas, reduce() podría ser más eficiente.
// Ejemplo de optimización con filter antes de map
const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Primero filtramos y luego transformamos (más eficiente)
const squaredEvens = numbers
  .filter(num => num % 2 === 0)
  .map(num => num * num);

console.log(squaredEvens); // [4, 16, 36, 64, 100]

El método map() es una herramienta fundamental para trabajar con colecciones de datos en TypeScript, permitiéndonos transformar información de manera declarativa y manteniendo la inmutabilidad de nuestros datos originales.

Filtrando colecciones con filter()

El método filter() es una herramienta esencial en la programación funcional con TypeScript que nos permite seleccionar elementos de un array basándonos en una condición específica. A diferencia de los bucles tradicionales, filter() nos proporciona una forma declarativa y concisa de extraer elementos que cumplan ciertos criterios.

Al igual que map(), el método filter() respeta el principio de inmutabilidad, creando un nuevo array con los elementos que pasan la prueba sin modificar el array original.

Sintaxis básica

La sintaxis de filter() es similar a la de otros métodos funcionales de arrays:

const resultado = array.filter(callback);

El callback es una función que evalúa cada elemento del array y debe devolver un valor booleano:

array.filter((currentValue, index, array) => {
  // Lógica de evaluación
  return true; // El elemento se incluye en el resultado
  // o
  return false; // El elemento se excluye del resultado
});
  • currentValue: El elemento actual que está siendo evaluado
  • index: La posición del elemento actual en el array
  • array: El array completo sobre el que se llamó filter()

Ejemplos básicos

Veamos un ejemplo simple donde filtramos números pares de un array:

const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers: number[] = numbers.filter(num => num % 2 === 0);

console.log(evenNumbers); // [2, 4, 6, 8, 10]
console.log(numbers); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - El array original no cambia

También podemos filtrar elementos basados en otros criterios, como la longitud de un string:

const words: string[] = ["apple", "banana", "kiwi", "strawberry", "fig"];
const shortWords: string[] = words.filter(word => word.length <= 5);

console.log(shortWords); // ["apple", "kiwi", "fig"]

Tipado en TypeScript

TypeScript nos permite definir claramente los tipos cuando usamos filter():

const numbers: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Explícitamente tipamos la función callback
const positiveNumbers: number[] = numbers.filter((num: number): boolean => {
  return num > 0;
});

// TypeScript infiere que el resultado sigue siendo number[]
const largeNumbers = numbers.filter(num => num > 5);

Filtrando arrays de objetos

Donde filter() realmente brilla es al trabajar con arrays de objetos, permitiéndonos extraer elementos basados en propiedades específicas:

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

const products: Product[] = [
  { id: 1, name: "Laptop", price: 999.99, category: "Electronics", inStock: true },
  { id: 2, name: "Headphones", price: 89.99, category: "Electronics", inStock: false },
  { id: 3, name: "Coffee Mug", price: 12.99, category: "Kitchen", inStock: true },
  { id: 4, name: "Smartphone", price: 699.99, category: "Electronics", inStock: true },
  { id: 5, name: "Blender", price: 49.99, category: "Kitchen", inStock: false }
];

// Filtrar productos disponibles
const availableProducts = products.filter(product => product.inStock);
console.log(availableProducts.length); // 3

// Filtrar productos electrónicos con precio menor a 100
const affordableElectronics = products.filter(product => 
  product.category === "Electronics" && product.price < 100
);
console.log(affordableElectronics); // [{ id: 2, name: "Headphones", ... }]

Casos de uso prácticos

Filtrado de datos para búsquedas

Un caso de uso común es implementar funcionalidad de búsqueda o filtrado en aplicaciones:

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  active: boolean;
}

const users: User[] = [
  { id: 1, name: "Ana García", email: "ana@example.com", role: "admin", active: true },
  { id: 2, name: "Carlos López", email: "carlos@example.com", role: "user", active: true },
  { id: 3, name: "Elena Martín", email: "elena@example.com", role: "user", active: false },
  { id: 4, name: "David Sánchez", email: "david@example.com", role: "editor", active: true }
];

// Función de búsqueda flexible
function searchUsers(query: string, userList: User[]): User[] {
  const lowercaseQuery = query.toLowerCase();
  
  return userList.filter(user => 
    user.name.toLowerCase().includes(lowercaseQuery) || 
    user.email.toLowerCase().includes(lowercaseQuery)
  );
}

const searchResults = searchUsers("ar", users);
console.log(searchResults);
// [
//   { id: 1, name: "Ana García", ... },
//   { id: 2, name: "Carlos López", ... }
// ]

// Filtrar por múltiples criterios
const activeAdmins = users.filter(user => user.active && user.role === "admin");
console.log(activeAdmins); // [{ id: 1, name: "Ana García", ... }]

Eliminación de duplicados

Podemos usar filter() junto con otros métodos para eliminar elementos duplicados:

const numbers: number[] = [1, 2, 2, 3, 4, 4, 5];

// Eliminar duplicados usando filter e indexOf
const uniqueNumbers = numbers.filter((value, index, self) => 
  self.indexOf(value) === index
);

console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

// Alternativa moderna usando Set (no requiere filter)
const uniqueWithSet = [...new Set(numbers)];
console.log(uniqueWithSet); // [1, 2, 3, 4, 5]

Combinando filter() con otros métodos

La verdadera potencia de filter() se aprecia cuando lo combinamos con otros métodos funcionales:

interface Task {
  id: number;
  title: string;
  completed: boolean;
  dueDate: string;
}

const tasks: Task[] = [
  { id: 1, title: "Completar informe", completed: false, dueDate: "2023-05-15" },
  { id: 2, title: "Revisar código", completed: true, dueDate: "2023-05-10" },
  { id: 3, title: "Preparar presentación", completed: false, dueDate: "2023-05-20" },
  { id: 4, title: "Enviar email", completed: false, dueDate: "2023-05-12" }
];

// Obtener títulos de tareas pendientes ordenadas por fecha de vencimiento
const pendingTasksByDueDate = tasks
  .filter(task => !task.completed)
  .sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())
  .map(task => `${task.title} (vence: ${task.dueDate})`);

console.log(pendingTasksByDueDate);
// [
//   "Enviar email (vence: 2023-05-12)",
//   "Completar informe (vence: 2023-05-15)",
//   "Preparar presentación (vence: 2023-05-20)"
// ]

Patrones avanzados con filter()

Filtrado condicional dinámico

Podemos crear funciones de filtrado más flexibles que acepten parámetros:

interface FilterOptions {
  minPrice?: number;
  maxPrice?: number;
  category?: string;
  onlyInStock?: boolean;
}

function filterProducts(products: Product[], options: FilterOptions): Product[] {
  return products.filter(product => {
    // Verificamos cada condición solo si está definida en las opciones
    if (options.minPrice !== undefined && product.price < options.minPrice) {
      return false;
    }
    
    if (options.maxPrice !== undefined && product.price > options.maxPrice) {
      return false;
    }
    
    if (options.category !== undefined && product.category !== options.category) {
      return false;
    }
    
    if (options.onlyInStock && !product.inStock) {
      return false;
    }
    
    return true;
  });
}

// Uso del filtro dinámico
const filteredProducts = filterProducts(products, {
  minPrice: 50,
  maxPrice: 500,
  onlyInStock: true
});

console.log(filteredProducts);
// Muestra productos en stock con precio entre 50 y 500

Filtrado con predicados tipados

Podemos mejorar la seguridad de tipos creando predicados tipados:

interface Vehicle {
  type: string;
  model: string;
}

interface Car extends Vehicle {
  type: "car";
  doors: number;
}

interface Motorcycle extends Vehicle {
  type: "motorcycle";
  engineSize: number;
}

// Unión de tipos
type Transport = Car | Motorcycle;

// Predicado tipado
function isCar(vehicle: Transport): vehicle is Car {
  return vehicle.type === "car";
}

const vehicles: Transport[] = [
  { type: "car", model: "Toyota Corolla", doors: 4 },
  { type: "motorcycle", model: "Honda CBR", engineSize: 600 },
  { type: "car", model: "Ford Focus", doors: 5 },
  { type: "motorcycle", model: "Yamaha MT-07", engineSize: 700 }
];

// TypeScript reconoce que el resultado solo contiene objetos de tipo Car
const cars: Car[] = vehicles.filter(isCar);

// Ahora podemos acceder con seguridad a la propiedad doors
const totalDoors = cars.reduce((sum, car) => sum + car.doors, 0);
console.log(totalDoors); // 9

El método filter() es una herramienta fundamental para trabajar con colecciones de datos en TypeScript, permitiéndonos seleccionar subconjuntos de elementos de manera declarativa y manteniendo la inmutabilidad de nuestros datos originales.

Acumulando valores con reduce()

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

El método reduce() es una de las herramientas más versátiles y potentes para procesar arrays en TypeScript. A diferencia de map() y filter(), que siempre devuelven arrays, reduce() nos permite transformar un array en cualquier tipo de valor: un número, un string, un objeto o incluso otro array.

La idea fundamental detrás de reduce() es acumular valores mientras recorremos los elementos de un array, aplicando una función que combina el acumulador con cada elemento para producir un único resultado final.

Sintaxis básica

La sintaxis de reduce() es ligeramente diferente a la de otros métodos funcionales:

const resultado = array.reduce(callback, valorInicial);

Donde:

  • callback: Función que se ejecuta en cada elemento del array
  • valorInicial: Valor con el que comienza el acumulador (opcional, pero recomendado)

El callback recibe cuatro parámetros:

array.reduce((accumulator, currentValue, index, array) => {
  // Lógica para combinar el acumulador con el valor actual
  return nuevoAcumulador;
}, valorInicial);
  • accumulator: El valor acumulado retornado en la iteración anterior
  • currentValue: El elemento actual que está siendo procesado
  • index: La posición del elemento actual en el array
  • array: El array completo sobre el que se llamó reduce()

Ejemplos básicos

Veamos un ejemplo clásico: sumar todos los números de un array:

const numbers: number[] = [1, 2, 3, 4, 5];
const sum: number = numbers.reduce((acc, current) => acc + current, 0);

console.log(sum); // 15

En este ejemplo:

  • Comenzamos con un acumulador inicial de 0
  • En cada iteración, sumamos el valor actual al acumulador
  • Al final, obtenemos la suma total de todos los elementos

La importancia del valor inicial

Aunque el valor inicial es técnicamente opcional, es una buena práctica siempre proporcionarlo para:

  1. Evitar errores con arrays vacíos
  2. Establecer claramente el tipo del resultado
  3. Hacer el código más predecible

Comparemos estos dos ejemplos:

// Sin valor inicial (usa el primer elemento como acumulador inicial)
const numbers: number[] = [1, 2, 3, 4, 5];
const sum1 = numbers.reduce((acc, current) => acc + current); // 15

// Con valor inicial explícito
const sum2 = numbers.reduce((acc, current) => acc + current, 0); // 15

// ¿Qué pasa con un array vacío?
const emptyArray: number[] = [];
// const errorSum = emptyArray.reduce((acc, current) => acc + current); // ¡ERROR!
const safeSum = emptyArray.reduce((acc, current) => acc + current, 0); // 0 (seguro)

Transformando tipos con reduce()

Una característica poderosa de reduce() es que el acumulador puede ser de un tipo diferente al del array:

const numbers: number[] = [1, 2, 3, 4, 5];

// Convertir array de números a string
const numberString: string = numbers.reduce(
  (text, num) => text + num.toString(), 
  ""
);

console.log(numberString); // "12345"

// Crear un objeto a partir de un array
interface NumberCount {
  even: number;
  odd: number;
}

const counts: NumberCount = numbers.reduce(
  (result, num) => {
    if (num % 2 === 0) {
      result.even += 1;
    } else {
      result.odd += 1;
    }
    return result;
  }, 
  { even: 0, odd: 0 } // Valor inicial como objeto
);

console.log(counts); // { even: 2, odd: 3 }

Casos de uso prácticos

Calculando totales en un carrito de compras

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

const shoppingCart: CartItem[] = [
  { id: 1, name: "Laptop", price: 999.99, quantity: 1 },
  { id: 2, name: "Mouse", price: 25.50, quantity: 2 },
  { id: 3, name: "Keyboard", price: 45.99, quantity: 1 }
];

// Calcular el total del carrito
const cartTotal = shoppingCart.reduce(
  (total, item) => total + (item.price * item.quantity),
  0
);

console.log(`Total: $${cartTotal.toFixed(2)}`); // "Total: $1096.98"

// Calcular múltiples valores en una sola pasada
interface CartSummary {
  totalPrice: number;
  itemCount: number;
  mostExpensiveItem: string;
  highestPrice: number;
}

const cartSummary = shoppingCart.reduce<CartSummary>(
  (summary, item) => {
    const itemTotal = item.price * item.quantity;
    
    // Actualizar el precio total
    summary.totalPrice += itemTotal;
    
    // Actualizar el conteo de items
    summary.itemCount += item.quantity;
    
    // Actualizar el item más caro si corresponde
    if (item.price > summary.highestPrice) {
      summary.highestPrice = item.price;
      summary.mostExpensiveItem = item.name;
    }
    
    return summary;
  },
  { totalPrice: 0, itemCount: 0, mostExpensiveItem: "", highestPrice: 0 }
);

console.log(cartSummary);
// {
//   totalPrice: 1096.98,
//   itemCount: 4,
//   mostExpensiveItem: "Laptop",
//   highestPrice: 999.99
// }

Agrupando datos

Uno de los usos más comunes de reduce() es agrupar elementos por alguna propiedad:

interface Student {
  id: number;
  name: string;
  grade: string;
}

const students: Student[] = [
  { id: 1, name: "Ana", grade: "A" },
  { id: 2, name: "Bruno", grade: "B" },
  { id: 3, name: "Carlos", grade: "A" },
  { id: 4, name: "Diana", grade: "C" },
  { id: 5, name: "Elena", grade: "B" }
];

// Agrupar estudiantes por calificación
interface StudentsByGrade {
  [grade: string]: Student[];
}

const groupedByGrade = students.reduce<StudentsByGrade>(
  (groups, student) => {
    // Si el grupo no existe, lo creamos
    if (!groups[student.grade]) {
      groups[student.grade] = [];
    }
    
    // Añadimos el estudiante al grupo correspondiente
    groups[student.grade].push(student);
    
    return groups;
  },
  {}
);

console.log(groupedByGrade);
// {
//   "A": [
//     { id: 1, name: "Ana", grade: "A" },
//     { id: 3, name: "Carlos", grade: "A" }
//   ],
//   "B": [
//     { id: 2, name: "Bruno", grade: "B" },
//     { id: 5, name: "Elena", grade: "B" }
//   ],
//   "C": [
//     { id: 4, name: "Diana", grade: "C" }
//   ]
// }

Implementando map() y filter() con reduce()

Una demostración del poder de reduce() es que podemos implementar otros métodos funcionales como map() y filter() usando reduce():

// Implementando map() con reduce()
function myMap<T, U>(array: T[], callback: (item: T, index: number) => U): U[] {
  return array.reduce<U[]>(
    (result, current, index) => {
      result.push(callback(current, index));
      return result;
    },
    []
  );
}

// Implementando filter() con reduce()
function myFilter<T>(array: T[], predicate: (item: T, index: number) => boolean): T[] {
  return array.reduce<T[]>(
    (result, current, index) => {
      if (predicate(current, index)) {
        result.push(current);
      }
      return result;
    },
    []
  );
}

// Probando nuestras implementaciones
const numbers = [1, 2, 3, 4, 5];

const doubled = myMap(numbers, num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

const evens = myFilter(numbers, num => num % 2 === 0);
console.log(evens); // [2, 4]

Patrones avanzados con reduce()

Aplanando arrays anidados (flatten)

const nestedArrays: number[][] = [[1, 2], [3, 4], [5, 6]];

const flattened: number[] = nestedArrays.reduce<number[]>(
  (result, current) => [...result, ...current],
  []
);

console.log(flattened); // [1, 2, 3, 4, 5, 6]

Construyendo una pipeline de funciones

type Transformer<T> = (data: T) => T;

function pipe<T>(...fns: Transformer<T>[]): Transformer<T> {
  return (initialValue: T): T => {
    return fns.reduce(
      (value, fn) => fn(value),
      initialValue
    );
  };
}

// Ejemplo de uso
const addFive = (x: number) => x + 5;
const multiplyByTwo = (x: number) => x * 2;
const subtractTen = (x: number) => x - 10;

const transformNumber = pipe(
  addFive,
  multiplyByTwo,
  subtractTen
);

console.log(transformNumber(5)); // ((5 + 5) * 2) - 10 = 10

Consejos para usar reduce() efectivamente

  • Siempre proporciona un valor inicial para evitar errores y hacer el código más predecible.
  • Usa tipado explícito para el acumulador cuando trabajes con tipos complejos.
  • Considera la legibilidad: para operaciones muy complejas, a veces es mejor usar otros métodos o dividir la lógica.
  • Mantén pura la función reductora: evita efectos secundarios dentro del callback.
// Usando tipado explícito para mayor claridad
interface Summary {
  total: number;
  average: number;
  min: number;
  max: number;
}

const numbers = [23, 45, 12, 67, 89, 34];

const stats = numbers.reduce<Summary>(
  (acc, num) => ({
    total: acc.total + num,
    average: 0, // Calcularemos esto al final
    min: Math.min(acc.min, num),
    max: Math.max(acc.max, num)
  }),
  { total: 0, average: 0, min: Infinity, max: -Infinity }
);

// Completamos el cálculo después de reduce
stats.average = numbers.length > 0 ? stats.total / numbers.length : 0;

console.log(stats);
// {
//   total: 270,
//   average: 45,
//   min: 12,
//   max: 89
// }

El método reduce() es una herramienta extremadamente versátil que nos permite expresar transformaciones complejas de datos de manera concisa y declarativa, siguiendo los principios de la programación funcional en TypeScript.

Tipado correcto de callbacks

Cuando trabajamos con métodos funcionales de arrays en TypeScript, el tipado correcto de callbacks es fundamental para aprovechar al máximo el sistema de tipos y evitar errores en tiempo de ejecución. Un callback bien tipado nos proporciona autocompletado, validación de tipos y mejor documentación de nuestro código.

Entendiendo la estructura de los callbacks

Antes de profundizar en el tipado, es importante entender la estructura básica de los callbacks en los métodos funcionales:

// Estructura general de callbacks en métodos de arrays
array.method((element, index, array) => {
  // Lógica del callback
  return result;
});

Cada método funcional (map, filter, reduce) espera un callback con una firma específica que TypeScript puede verificar.

Tipado implícito vs. explícito

TypeScript puede inferir automáticamente los tipos en muchos casos, pero a veces es mejor ser explícito:

// Tipado implícito (inferencia)
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);

// Tipado explícito
const tripled = numbers.map((num: number): number => num * 3);

El tipado explícito es especialmente útil cuando:

  • Trabajamos con tipos complejos
  • Necesitamos mayor claridad en el código
  • Queremos documentar mejor las transformaciones

Tipado de callbacks en map()

El método map() espera un callback que reciba un elemento y devuelva un valor transformado:

// Tipado básico
const numbers: number[] = [1, 2, 3, 4, 5];
const doubled = numbers.map((num: number): number => num * 2);

// Transformando a otro tipo
const numberToString = numbers.map((num: number): string => `Number: ${num}`);

// Usando todos los parámetros
const withIndexes = numbers.map((num: number, index: number, arr: number[]): string => {
  return `Index ${index}: ${num} (from array of length ${arr.length})`;
});

Para transformaciones más complejas, el tipado explícito ayuda a clarificar la intención:

interface Person {
  id: number;
  name: string;
}

interface PersonDTO {
  userId: string;
  displayName: string;
}

const people: Person[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" }
];

const dtos: PersonDTO[] = people.map((person: Person): PersonDTO => ({
  userId: `user-${person.id}`,
  displayName: person.name.toUpperCase()
}));

Tipado de callbacks en filter()

Para filter(), el callback debe devolver un booleano que determine si el elemento se incluye:

const numbers: number[] = [1, 2, 3, 4, 5, 6];

// Tipado básico
const evenNumbers = numbers.filter((num: number): boolean => num % 2 === 0);

// Usando predicados tipados para filtrar por tipo
interface Vehicle {
  type: string;
  year: number;
}

interface Car extends Vehicle {
  type: "car";
  doors: number;
}

interface Bike extends Vehicle {
  type: "bike";
  frameSize: number;
}

const vehicles: (Car | Bike)[] = [
  { type: "car", year: 2020, doors: 4 },
  { type: "bike", year: 2019, frameSize: 54 },
  { type: "car", year: 2018, doors: 2 }
];

// Predicado tipado
function isCar(vehicle: Car | Bike): vehicle is Car {
  return vehicle.type === "car";
}

// TypeScript sabe que el resultado solo contiene Cars
const cars: Car[] = vehicles.filter(isCar);
console.log(cars[0].doors); // Acceso seguro a la propiedad doors

Tipado de callbacks en reduce()

El tipado de reduce() es más complejo porque involucra dos tipos: el del array original y el del acumulador:

const numbers: number[] = [1, 2, 3, 4, 5];

// Tipado básico (acumulador del mismo tipo que los elementos)
const sum = numbers.reduce(
  (acc: number, current: number): number => acc + current, 
  0
);

// Acumulador de tipo diferente
const numberToString = numbers.reduce(
  (text: string, num: number): string => text + num.toString(),
  ""
);

// Tipado completo con genéricos
interface Summary {
  sum: number;
  count: number;
  average: number;
}

const stats = numbers.reduce<Summary>(
  (acc: Summary, num: number): Summary => ({
    sum: acc.sum + num,
    count: acc.count + 1,
    average: (acc.sum + num) / (acc.count + 1)
  }),
  { sum: 0, count: 0, average: 0 }
);

Usando tipos genéricos para callbacks reutilizables

Podemos crear funciones de callback reutilizables con tipos genéricos:

// Función genérica para extraer una propiedad
function pluck<T, K extends keyof T>(array: T[], key: K): T[K][] {
  return array.map(item => item[key]);
}

interface Product {
  id: number;
  name: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: "Laptop", price: 999 },
  { id: 2, name: "Mouse", price: 25 },
  { id: 3, name: "Keyboard", price: 45 }
];

// TypeScript infiere correctamente los tipos de retorno
const names: string[] = pluck(products, "name");
const prices: number[] = pluck(products, "price");

// Error de tipo si intentamos acceder a una propiedad inexistente
// const invalid = pluck(products, "category"); // Error!

Tipado de callbacks encadenados

Cuando encadenamos métodos, TypeScript infiere los tipos en cada paso:

interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: number;
}

const tasks: Task[] = [
  { id: 1, title: "Learn TypeScript", completed: false, priority: 1 },
  { id: 2, title: "Update resume", completed: true, priority: 2 },
  { id: 3, title: "Prepare presentation", completed: false, priority: 3 }
];

// Encadenamiento con inferencia de tipos
const highPriorityTitles = tasks
  .filter((task: Task): boolean => task.priority > 1 && !task.completed)
  .map((task: Task): string => task.title.toUpperCase());

// TypeScript sabe que highPriorityTitles es string[]
console.log(highPriorityTitles); // ["PREPARE PRESENTATION"]

Usando tipos de utilidad para callbacks

TypeScript proporciona tipos de utilidad que pueden ayudar con el tipado de callbacks:

// Definiendo un tipo para callbacks de map
type MapCallback<T, U> = (value: T, index: number, array: T[]) => U;

// Definiendo un tipo para callbacks de filter
type FilterCallback<T> = (value: T, index: number, array: T[]) => boolean;

// Definiendo un tipo para callbacks de reduce
type ReduceCallback<T, U> = (accumulator: U, value: T, index: number, array: T[]) => U;

// Usando estos tipos
function customMap<T, U>(array: T[], callback: MapCallback<T, U>): U[] {
  return array.map(callback);
}

const numbers = [1, 2, 3, 4, 5];
const squared = customMap(numbers, (n: number): number => n * n);

Errores comunes y cómo evitarlos

1. Olvidar el valor inicial en reduce()

// ❌ Incorrecto: sin valor inicial y sin tipo explícito
const sum = [1, 2, 3].reduce((acc, num) => acc + num);

// ✅ Correcto: con valor inicial y tipo explícito
const safeSum = [1, 2, 3].reduce<number>((acc, num) => acc + num, 0);

2. Tipos incompatibles en el callback

// ❌ Incorrecto: el callback devuelve un tipo incompatible
const numbers: number[] = [1, 2, 3];
const invalid = numbers.map((num: number): string => num * 2); // Error de tipo

// ✅ Correcto: el tipo de retorno coincide con la operación
const valid = numbers.map((num: number): number => num * 2);

3. No usar predicados tipados para filtrar por tipo

interface Animal { name: string; }
interface Dog extends Animal { breed: string; }
interface Cat extends Animal { lives: number; }

const animals: (Dog | Cat)[] = [
  { name: "Rex", breed: "German Shepherd" },
  { name: "Whiskers", lives: 9 }
];

// ❌ Incorrecto: TypeScript no sabe que estamos filtrando por tipo
const dogs = animals.filter(animal => "breed" in animal);
// dogs[0].breed; // Error: Property 'breed' does not exist on type 'Dog | Cat'

// ✅ Correcto: usando un predicado tipado
function isDog(animal: Dog | Cat): animal is Dog {
  return "breed" in animal;
}

const typedDogs = animals.filter(isDog);
console.log(typedDogs[0].breed); // Acceso seguro

Mejores prácticas para el tipado de callbacks

  • Usa tipos explícitos para parámetros y valores de retorno en callbacks complejos.
  • Proporciona siempre un valor inicial tipado para reduce().
  • Utiliza predicados tipados cuando filtres por tipo.
  • Crea tipos reutilizables para callbacks comunes.
  • Aprovecha la inferencia de tipos cuando sea apropiado para código más conciso.
  • Usa genéricos para crear funciones de orden superior tipadas.
// Ejemplo de función de orden superior bien tipada
function createFilterByProperty<T, K extends keyof T>(
  property: K, 
  value: T[K]
): (item: T) => boolean {
  return (item: T): boolean => item[property] === value;
}

interface User {
  id: number;
  role: string;
  active: boolean;
}

const users: User[] = [
  { id: 1, role: "admin", active: true },
  { id: 2, role: "user", active: false },
  { id: 3, role: "user", active: true }
];

// Creamos filtros tipados reutilizables
const filterByAdmin = createFilterByProperty("role", "admin");
const filterByActive = createFilterByProperty("active", true);

// Aplicamos los filtros
const activeAdmins = users.filter(filterByAdmin).filter(filterByActive);

El tipado correcto de callbacks no solo mejora la seguridad de tipos en nuestro código, sino que también proporciona una mejor experiencia de desarrollo con autocompletado y documentación integrada, haciendo que nuestro código sea más mantenible y menos propenso a errores.

Aprendizajes de esta lección de TypeScript

  • Dominar el uso de map() para transformar cada elemento de un array generando una nueva colección sin modificar la original
  • Aplicar filter() para seleccionar elementos que cumplan condiciones específicas manteniendo la inmutabilidad de los datos
  • Utilizar reduce() para acumular valores y transformar arrays en cualquier tipo de resultado (números, strings, objetos)
  • Implementar el tipado correcto de callbacks para garantizar la seguridad de tipos al trabajar con métodos funcionales
  • Combinar estos métodos para crear pipelines de transformación de datos expresivas y eficientes en aplicaciones TypeScript

Completa este curso de TypeScript y certifícate

Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración