TypeScript

TypeScript

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

Aprende a usar map, filter y reduce en TypeScript para transformar, filtrar y acumular datos en arrays con ejemplos prácticos y tipado correcto.

Aprende TypeScript y certifícate

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()

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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende TypeScript online

Ejercicios de esta lección Métodos funcionales de arrays (map, filter, reduce)

Evalúa tus conocimientos de esta lección Métodos funcionales de arrays (map, filter, reduce) 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

Proyecto calculadora gastos

TypeScript
Proyecto

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

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

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

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad

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

  • 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