Fundamentos JavaScript
Curso·De JavaScript a TypeScript estrictoRedacción asistida por IA

Recorrer datos: map, filter, find, reduce y Object.*

Lección 4 de 43 · El lenguaje: de JavaScript a TypeScript estricto

Cesar Enrique Manzano Velasco
Cesar Enrique Manzano Velasco
29 de agosto de 2026
Recorrer datos: map, filter, find, reduce y Object.*
De JavaScript a TypeScript estrictoLección 4/4

Temario del curso

De JavaScript a TypeScript estricto

Lección 4 de 4

CursoDe JavaScript a TypeScript estricto · Lección 4 de 4

Un servicio real pasa la mayor parte de su tiempo recorriendo colecciones: transformar una lista de registros, quedarse con los que cumplen algo, buscar uno concreto, acumular un total.

Existe un método específico para cada una de esas intenciones, y usar el correcto hace que la línea se lea como lo que hace. Esta lección repasa cuál corresponde a cada caso, desarma reduce en tres niveles y termina en lo que devuelven realmente Object.keys, Object.values y Object.entries, que reserva alguna sorpresa.

Nota sobre la terminología: en este artículo, callback significa la función que se le pasa a un método de recorrido para que la ejecute por cada elemento.

Los métodos de recorrido y la intención que comunican

Todos estos métodos reciben un callback que se ejecuta por cada elemento. La diferencia está en qué devuelven, y esa diferencia es la que comunica la intención a quien lee.

MétodoDevuelveSe usa paramapUn array del mismo tamañoTransformar cada elementofilterUn array igual o más cortoQuedarse con un subconjuntofindUn elemento o undefinedLocalizar el primero que cumplesome / everyUn booleanResponder una pregunta sobre la colecciónreduceLo que se decidaAcumular en un único valor

Vamos uno por uno.

map: transformar sin cambiar el tamaño

ts

const nombres = usuarios.map(usuario => usuario.nombre);

La garantía de map es que el resultado tiene exactamente tantos elementos como el original. Eso lo hace predecible, y también marca cuándo no usarlo.

Si la transformación a veces no debe producir nada, map te obliga a devolver algo igualmente, normalmente undefined, y el resultado queda contaminado:

ts

// mal: quedan huecos undefined y el tipo se ensucia const activos = usuarios.map(u => (u.activo ? u.nombre : undefined)); // (string | undefined)[] // bien: filtrar primero, transformar después const activos = usuarios.filter(u => u.activo).map(u => u.nombre); // string[]

TypeScript infiere el tipo del resultado a partir de lo que devuelve el callback, sin necesidad de anotarlo.

filter: quedarse con un subconjunto

ts

const pendientes = pedidos.filter(pedido => pedido.estado === 'PENDIENTE');

El detalle propio de TypeScript es que filter no estrecha el tipo por sí solo.

Definición — Estrechar (narrowing): Que el compilador pase de considerar un tipo amplio a uno más concreto porque una comprobación descartó posibilidades.

Filtrar los null de un array no cambia el tipo del resultado:

ts

const valores: (string | null)[] = ['a', null, 'b']; const limpios = valores.filter(v => v !== null); // sigue siendo (string | null)[]

El compilador no puede saber que esa condición elimina exactamente los null. La solución pasa por una guarda de tipo, que tiene su propia lección más adelante. Por ahora basta con saber que el filtro corrige el contenido pero no el tipo.

find, some y every: buscar y preguntar

ts

const contacto = contactos.find(c => c.tipo === 'email'); // Contacto | undefined const hayEmail = contactos.some(c => c.tipo === 'email'); // boolean const todosOk = lineas.every(l => l.cantidad > 0); // boolean

find devuelve el primer elemento que cumple, y undefined si no hay ninguno. Ese | undefined en el tipo es intencional: es lo que te obliga a comprobar antes de usar el resultado. Ignorarlo con ! es la forma más común de convertir un dato ausente en un error de producción, y tiene su propia lección más adelante.

Si solo te interesa saber si existe algo, y no cuál, some comunica mejor la intención que un find cuyo resultado se compara con undefined. Y every responde la pregunta inversa: si todos cumplen.

Ten en cuenta: dentro del callback, un return termina esa iteración, no el recorrido. find y some sí paran en cuanto encuentran; map y filter recorren siempre la colección entera.


reduce en tres niveles

reduce es el más general de todos. De hecho, todos los anteriores podrían escribirse con él, y por eso es el que peor se entiende. Vamos a construirlo por partes.

Definición — Acumulado: El valor que reduce va arrastrando de una iteración a la siguiente. Empieza siendo el valor inicial y termina siendo el resultado.

Sintaxis básica

reduce recibe dos argumentos: un callback y un valor inicial. El callback recibe el acumulado hasta ahora y el elemento actual, y devuelve el nuevo acumulado.

ts

const numeros = [1, 2, 3, 4]; const total = numeros.reduce((acc, actual) => acc + actual, 0); // 10

Paso a paso, con valor inicial 0:

IteraciónaccactualDevuelve10112123333646410

Lo único que hay que entender es que lo que devuelve el callback es el acc de la vuelta siguiente. Todo lo demás es consecuencia de eso.

Pasa siempre el valor inicial. Sin él, reduce toma el primer elemento como acumulado y falla con un error si el array está vacío. Además, el valor inicial le indica al compilador de qué tipo es el acumulado.

Nivel 2: acumular un array

El acumulado no tiene por qué ser un número. Si es un array, reduce puede hacer lo mismo que un map y un filter combinados:

ts

const positivos = numeros.reduce<number[]>((acc, actual) => { if (actual <= 0) return acc; // se descarta: se devuelve el acumulado igual return [...acc, actual * 2]; // se transforma y se añade }, []);

Fíjate en el parámetro de tipo explícito, reduce<number[]>. Sin él, el compilador infiere el tipo del acumulado a partir del valor inicial [], que no aporta información suficiente. Decirle el tipo es lo que hace que el resto del callback compile.

Aunque se puede, un filter().map() se lee mejor que este reduce cuando el resultado es un array. reduce gana cuando el resultado no es un array.

Nivel 3: acumular un objeto

Este es el caso que justifica aprender reduce: convertir una lista en un objeto indexado por alguna clave, algo que ningún otro método hace directamente.

ts

const unidadesPorSku = lineas.reduce<Record<string, number>>( (acc, linea) => ({ ...acc, [linea.sku]: linea.cantidad }), {}, );

Aquí se combinan tres cosas de la lección anterior: el acumulado es un objeto, se compone con spread para no modificarlo, y la clave es computada porque sale del propio dato.

Record<string, number> describe un objeto de claves de texto cuyos valores son números. Tiene su propia lección más adelante.

Una variante frecuente: sumar en lugar de sobrescribir cuando la clave se repite.

ts

const unidadesPorSku = lineas.reduce<Record<string, number>>((acc, linea) => { acc[linea.sku] = (acc[linea.sku] ?? 0) + linea.cantidad; return acc; }, {});

Esta versión modifica el acumulado en vez de recrearlo. Es una excepción razonable a la regla de no mutar: el objeto es local a la operación y nadie más lo ve, y evita crear un objeto nuevo en cada iteración, lo que con listas grandes marca diferencia real de rendimiento.

El atajo para elegir método

Tres preguntas, en este orden:

  1. ¿El resultado tiene el mismo tamaño? → map

  2. ¿Es un subconjunto? → filter

  3. ¿Es un solo valor con otra forma? → reduce


Object.keys, values y entries

Hasta aquí hemos recorrido arrays. Estas tres funciones permiten recorrer un objeto como si fuera una colección.

Definición — Propiedad propia y enumerable: Una propiedad que pertenece al objeto en sí, no heredada de su prototipo, y que aparece al recorrerlo. Los métodos de una clase no cumplen esta condición, porque viven en el prototipo.

Las tres recorren las propiedades propias y enumerables, y devuelven respectivamente sus claves, sus valores y sus pares:

ts

const config = { host: 'localhost', puerto: 5432 }; Object.keys(config); // ['host', 'puerto'] Object.values(config); // ['localhost', 5432] Object.entries(config); // [['host', 'localhost'], ['puerto', 5432]]

Combinadas con los métodos de array, permiten recorrer el objeto entero:

ts

Object.entries(config).forEach(([clave, valor]) => { console.log(`${clave}=${valor}`); });

Y Object.fromEntries hace el camino de vuelta, lo que da una alternativa más legible al reduce que construye un objeto:

ts

const unidadesPorSku = Object.fromEntries( lineas.map(linea => [linea.sku, linea.cantidad]), );

La sorpresa: Object.keys devuelve string[]

Aquí está el detalle que descoloca la primera vez. Aunque el compilador sepa perfectamente qué campos tiene el objeto, Object.keys devuelve string[], no la unión de sus claves:

ts

const config = { host: 'localhost', puerto: 5432 }; Object.keys(config).forEach(clave => { config[clave]; // ❌ error: `clave` es string, y `config` no acepta cualquier string });

No es un descuido, es una decisión deliberada y correcta. En TypeScript, un objeto puede tener más propiedades de las que declara su tipo: un valor con campos extra es asignable a un tipo con menos campos. Así que en ejecución Object.keys puede devolver claves que el tipo no menciona. Si el compilador prometiera que las claves son exactamente las declaradas, estaría mintiendo.

Object.entries arrastra la misma decisión: las claves llegan como string y los valores como la unión de los tipos de todos los campos.

Las salidas habituales:

SituaciónQué hacerSolo necesitas el par y no importa el tipo exacto de la claveRecorrer con Object.entries y desestructurarSabes que no habrá campos extraAserción sobre el resultado de keys, asumiendo la responsabilidadEl conjunto de claves es cerrado y conocidoPartir de una unión de claves declarada, no del objeto

Adelanto: la tercera opción es la que el curso desarrolla más adelante con Record y keyof, y es la única de las tres en la que el compilador sigue comprobando algo.

Ten en cuenta además que Object.keys solo recorre propiedades propias y enumerables. Aplicado a una instancia de clase, devuelve sus campos pero no sus métodos.


Ejercicios

Ejercicio 1: elegir el método correcto

Reescribe cada una de estas tres líneas con el método que mejor comunica la intención, sin cambiar el resultado.

Contexto: registros tiene la forma { error?: string }[], usuarios tiene { activo: boolean }[] y lineas tiene { cantidad: number; precio: number }[].

ts

const hayFallidos = registros.filter(r => r.error).length > 0; const primerActivo = usuarios.filter(u => u.activo)[0]; const totales = []; for (const linea of lineas) totales.push(linea.cantidad * linea.precio);

Ejercicio 2: el map que ensucia el tipo

Dado un array de contactos con la forma { tipo: string; valor: string }, escribe una expresión que devuelva un string[] con los valores de los contactos de tipo 'email'. Explica por qué hacerlo con un solo map produce un tipo peor.

Ejercicio 3: reduce que agrupa

A partir de un array de pedidos con la forma { id: string; estado: string }, produce un objeto que cuente cuántos pedidos hay en cada estado.

Resuélvelo primero con reduce y después comprueba si Object.fromEntries da una versión más legible.

Ejercicio 4: recorrer un objeto sin perder el tipo

Toma un objeto de configuración con campos conocidos e intenta recorrerlo con Object.keys accediendo a cada valor. Observa el error del compilador y explica en dos frases por qué Object.keys no puede devolver la unión de las claves declaradas.


Soluciones

Solución 1

ts

const hayFallidos = registros.some(r => r.error); const primerActivo = usuarios.find(u => u.activo); const totales = lineas.map(linea => linea.cantidad * linea.precio);

Los tres cambios comunican mejor la intención y además son más eficientes: some y find paran en cuanto encuentran, mientras que filter recorre siempre la lista completa.

Ojo con el segundo: find devuelve Contacto | undefined, igual que filter(...)[0], pero con el tipo correcto. La versión original devolvía un tipo que decía Usuario y podía valer undefined.

Solución 2

ts

const emails = contactos .filter(c => c.tipo === 'email') .map(c => c.valor); // string[]

Con un solo map tendrías que devolver algo en el caso que no interesa:

ts

const emails = contactos.map(c => (c.tipo === 'email' ? c.valor : undefined)); // (string | undefined)[]

El tipo es peor porque arrastra un undefined que ya no corresponde a nada real, y obliga a comprobarlo en cada uso posterior. Filtrar primero elimina el problema en el origen.

Solución 3

Con reduce:

ts

const porEstado = pedidos.reduce<Record<string, number>>((acc, pedido) => { acc[pedido.estado] = (acc[pedido.estado] ?? 0) + 1; return acc; }, {});

Object.fromEntries no da una versión más legible en este caso, y merece la pena entender por qué: sirve cuando cada elemento produce una entrada independiente, pero aquí varias líneas comparten clave y hay que sumarlas. Agrupar con acumulación es justo el caso donde reduce sigue siendo la herramienta correcta.

Solución 4

ts

const config = { host: 'localhost', puerto: 5432 }; Object.keys(config).forEach(clave => { config[clave]; // ❌ error: el índice de tipo string no existe en el tipo });

Object.keys devuelve string[] porque en TypeScript un objeto puede tener más propiedades de las que declara su tipo. Un valor con campos extra sigue siendo asignable a un tipo con menos campos, así que en ejecución pueden aparecer claves que el tipo no menciona.

Prometer la unión exacta de las claves declaradas sería una garantía que el lenguaje no puede cumplir.


Conclusión

Cada método de recorrido comunica una intención distinta, y elegir el que corresponde hace que la línea se explique sola: map cuando el tamaño se mantiene, filter cuando es un subconjunto, find o some cuando se busca, y reduce cuando el resultado tiene otra forma.

Los dos detalles que conviene no olvidar son que filter limpia el contenido pero no el tipo, y que Object.keys devuelve string[] a propósito, porque un objeto siempre puede traer más campos de los que su tipo declara.

Cesar Enrique Manzano Velasco

Sobre Cesar Enrique Manzano Velasco

Ingeniero Full Stack & AI/ML practitioner. Builder en AtaraxiaTech. Construyo microSaaS con IA en producción y lo cuento todo en público.