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ícateTransformando 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:
- Evitar errores con arrays vacíos
- Establecer claramente el tipo del resultado
- 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.
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
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Proyecto calculadora gastos
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
Todas las lecciones de TypeScript
Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Typescript
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales
Tipos Intermedios Y Avanzados
Tipos Genéricos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- 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