Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

map, filter e reduce em JavaScript com exemplos

map devolve um array do mesmo tamanho, filter devolve menos ou igual e reduce devolve qualquer coisa. Com os dois erros que cada um provoca no Node.

Rodolfo Mori8 min de leitura

Os três recebem uma função e percorrem o array. A diferença está inteira no que sai do outro lado: map devolve um array do mesmo tamanho, filter devolve um array de tamanho menor ou igual com os itens intactos, e reduce devolve qualquer coisa — um número, uma string, um objeto, outro array.

Guardar essa regra do tamanho resolve 90% das dúvidas de “qual eu uso aqui”. Precisa de uma lista de preços a partir de uma lista de produtos? Mesmo número de itens, map. Precisa só dos produtos em estoque? Menos itens, filter. Precisa do total do carrinho? Um número, reduce.

Os três dependem de você entender funções passadas como argumento, e nenhum deles altera o array original — são o contraponto imutável dos métodos que aparecem em adicionar e remover elementos de um array.

Os três métodos fazem transformações declarativas: você descreve o que deve acontecer com cada item, e o método controla a passagem pelo array. Em palavras simples, a escolha começa pelo formato da saída que você precisa.

Três esteiras, três formatos de saída

Num centro de distribuição, uma esteira troca a etiqueta de cada caixa, outra deixa passar apenas caixas aprovadas e uma terceira pesa todas para produzir um total. map corresponde uma entrada a uma saída, filter seleciona entradas inteiras e reduce acumula tudo em outro tipo de resultado. A callback muda, mas o contrato de cada esteira permanece.

Antes do primeiro exemplo, escreva apenas o tipo e o tamanho esperados da saída. Depois rode e confira com Array.isArray, length e o valor final. Faça essa previsão antes de escolher o método: se a forma esperada não combina com o contrato, a API escolhida está errada.

js
const carrinho = [
  { nome: 'Teclado mecânico', preco: 289.9 },
  { nome: 'Mouse sem fio', preco: 149.9 },
  { nome: 'Headset', preco: 399 },
];

const nomes = carrinho.map((item) => item.nome);
const comImposto = carrinho.map((item) => ({ ...item, preco: +(item.preco * 1.18).toFixed(2) }));

console.log(nomes);
console.log(comImposto);
console.log(carrinho.length, nomes.length, comImposto.length);
[ 'Teclado mecânico', 'Mouse sem fio', 'Headset' ] [ { nome: 'Teclado mecânico', preco: 342.08 }, { nome: 'Mouse sem fio', preco: 176.88 }, { nome: 'Headset', preco: 470.82 } ] 3 3 3

A última linha é a regra em forma de saída: entraram três, saíram três, nas duas transformações. map nunca filtra. Se você quer três de cinco, o método é outro.

Repare nos parênteses em (item) => ({ ... }). Sem eles, a arrow entenderia a chave como início de bloco e a função devolveria undefined — exatamente o problema da próxima seção.

map sem return devolve um array de undefined

js
const precos = [289.9, 149.9, 399];

const errado = precos.map((preco) => {
  preco * 1.18;
});

const certo = precos.map((preco) => {
  return +(preco * 1.18).toFixed(2);
});

console.log(errado);
console.log(certo);
[ undefined, undefined, undefined ] [ 342.08, 176.88, 470.82 ]

Três undefined, um para cada item — porque map grava o retorno do callback em cada posição, e um callback sem return retorna undefined. O tamanho continua certo; só o conteúdo é lixo.

Não é erro em tempo de execução: nada quebra aqui. O programa segue e a tela mostra três linhas vazias, ou três NaN depois de uma conta. Esse é o custo de map não reclamar.

filter: mantém ou descarta, nunca transforma

O callback do filter responde uma pergunta de sim ou não. O item que passa entra no array novo exatamente como estava:

js
const catalogo = [
  { nome: 'Teclado mecânico', preco: 289.9, estoque: 4 },
  { nome: 'Mouse sem fio', preco: 149.9, estoque: 0 },
  { nome: 'Headset', preco: 399, estoque: 9 },
  { nome: 'Webcam', preco: 219.9, estoque: 2 },
];

const disponiveis = catalogo.filter((p) => p.estoque > 0);
const caros = catalogo.filter((p) => p.preco > 1000);

console.log(disponiveis.map((p) => p.nome));
console.log(caros);
console.log(catalogo.length, disponiveis.length, caros.length);
[ 'Teclado mecânico', 'Headset', 'Webcam' ] [] 4 3 0

Quatro entraram, três passaram no primeiro filtro, zero no segundo. Quando nada passa, filter devolve array vazio — nunca null, nunca undefined. Isso é o que permite encadear sem checar nada: .filter(...).map(...) sobre uma lista vazia continua funcionando.

O erro de trocar filter por map

js
const catalogo = [
  { nome: 'Teclado mecânico', preco: 289.9 },
  { nome: 'Mouse sem fio', preco: 149.9 },
];

const errado = catalogo.filter((p) => p.nome);
const certo = catalogo.map((p) => p.nome);

console.log(errado);
console.log(certo);

const cupons = ['PRIMEIRACOMPRA', '', null, 'FRETEGRATIS', undefined];
console.log(cupons.filter(Boolean));
[ { nome: 'Teclado mecânico', preco: 289.9 }, { nome: 'Mouse sem fio', preco: 149.9 } ] [ 'Teclado mecânico', 'Mouse sem fio' ] [ 'PRIMEIRACOMPRA', 'FRETEGRATIS' ]

O primeiro filter devolveu os objetos inteiros. Ele avaliou p.nome como verdadeiro ou falso — e toda string não vazia é verdadeira, então nada foi descartado. Quem esperava uma lista de nomes recebeu a lista original.

A terceira saída mostra o uso legítimo dessa mesma mecânica: filter(Boolean) descarta '', null, undefined, 0 e NaN de uma vez. É o jeito idiomático de limpar uma lista que veio suja de um formulário ou de uma API.

reduce: dobra o array num valor só

reduce(callback, valorInicial). O callback recebe o acumulado e o item, e o que ele devolve vira o acumulado da próxima volta:

js
const carrinho = [
  { nome: 'Teclado mecânico', preco: 289.9, quantidade: 1 },
  { nome: 'Mouse sem fio', preco: 149.9, quantidade: 2 },
  { nome: 'Headset', preco: 399, quantidade: 1 },
];

const total = carrinho.reduce((soma, item) => soma + item.preco * item.quantidade, 0);
const pecas = carrinho.reduce((n, item) => n + item.quantidade, 0);
const maisCaro = carrinho.reduce((a, b) => (a.preco > b.preco ? a : b));

console.log('R$', total.toFixed(2));
console.log(pecas, 'peças');
console.log(maisCaro.nome);
R$ 988.70 4 peças Headset

Três reduções sobre o mesmo array, com três saídas de tipos diferentes: um número com centavos, uma contagem e um objeto. É isso que “qualquer coisa” significa.

O terceiro é o único caso comum em que não se passa valor inicial: sem ele, o primeiro item vira o acumulado de partida e a comparação começa item contra item. Passar 0 ali quebraria a lógica, porque 0.preco não existe.

O erro: Reduce of empty array with no initial value

js
const carrinho = [];

const total = carrinho.reduce((soma, item) => soma + item.preco);

console.log(total);
file:///private/tmp/loja/total-vazio.mjs:3 const total = carrinho.reduce((soma, item) => soma + item.preco); ^ TypeError: Reduce of empty array with no initial value at Array.reduce (<anonymous>) at file:///private/tmp/loja/total-vazio.mjs:3:24 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) at async node:internal/modules/esm/loader:633:26 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5) Node.js v24.16.0

Sem valor inicial, o reduce usa o primeiro item do array como ponto de partida. Em array vazio não existe primeiro item, não existe nada para devolver — e a especificação manda lançar TypeError em vez de inventar um zero.

O que torna esse erro traiçoeiro é que ele passa em todos os seus testes. O carrinho de exemplo tem itens, o array de mock tem itens; o array vazio só aparece na produção, no cliente que abriu o carrinho pela primeira vez. É um erro de caso de borda, não de lógica.

A correção é um caractere:

js
const carrinho = [];

const total = carrinho.reduce((soma, item) => soma + item.preco, 0);
console.log(total);

const umItem = [{ nome: 'Headset', preco: 399 }];
let chamadas = 0;
const resultado = umItem.reduce((acumulado, item) => {
  chamadas += 1;
  return acumulado + item.preco;
});

console.log(resultado, 'callback chamado', chamadas, 'vez(es)');
0 { nome: 'Headset', preco: 399 } callback chamado 0 vez(es)

A primeira linha é o total correto de um carrinho vazio: zero.

A segunda é o irmão silencioso do mesmo problema. Um array com um item e sem valor inicial: o reduce devolveu o objeto inteiro, não o preço, e o callback nunca foi chamado — o contador provou. Nenhum erro, nenhum aviso, e o total do pedido virou [object Object] na tela.

Encadear filter e map

A força dos três aparece quando eles se combinam. Cada método devolve um array, então o próximo já pode ser chamado no mesmo ponto:

js
const catalogo = [
  { nome: 'Teclado mecânico', preco: 289.9, estoque: 4 },
  { nome: 'Mouse sem fio', preco: 149.9, estoque: 0 },
  { nome: 'Headset', preco: 399, estoque: 9 },
  { nome: 'Webcam', preco: 219.9, estoque: 2 },
];

const etiquetas = catalogo
  .filter((p) => p.estoque > 0)
  .map((p) => `${p.nome}: R$ ${p.preco.toFixed(2)}`);

const totalDisponivel = catalogo
  .filter((p) => p.estoque > 0)
  .reduce((soma, p) => soma + p.preco * p.estoque, 0);

console.log(etiquetas);
console.log('R$', totalDisponivel.toFixed(2));
[ 'Teclado mecânico: R$ 289.90', 'Headset: R$ 399.00', 'Webcam: R$ 219.90' ] R$ 5190.40

Ordem importa e economiza trabalho: filtre antes de mapear. Mapear os quatro para depois descartar um é uma transformação jogada fora. Com quatro produtos não muda nada; com quatro mil, muda.

O que não vale a pena é ter medo do custo. Cada método faz uma passada, e um encadeamento de dois faz duas — mas você trocou um laço com if dentro e uma variável de acumulação por duas linhas que dizem exatamente o que fazem.

map que devolve listas: use flatMap

Um caso em que a regra do “mesmo tamanho” atrapalha:

js
const pedidos = [
  { numero: 1041, itens: ['Teclado mecânico', 'Mouse sem fio'] },
  { numero: 1042, itens: ['Headset'] },
  { numero: 1043, itens: [] },
];

const comMap = pedidos.map((p) => p.itens);
const comFlatMap = pedidos.flatMap((p) => p.itens);

console.log(comMap);
console.log(comFlatMap);
console.log(comMap.length, comFlatMap.length);
[ [ 'Teclado mecânico', 'Mouse sem fio' ], [ 'Headset' ], [] ] [ 'Teclado mecânico', 'Mouse sem fio', 'Headset' ] 3 3

O map cumpriu o contrato à risca: três pedidos, três itens de saída — só que cada um é um array, inclusive o vazio do pedido 1043. flatMap desmonta um nível e entrega a lista corrida.

Repare que os dois length são 3 por coincidência: o comFlatMap tem três produtos, o comMap tem três listas. Some um pedido de dois itens e a coincidência acaba — contar a coisa errada aqui faz o resumo do pedido sair com o número trocado.

Agrupar com reduce

O uso de reduce que mais aparece em código de verdade não é somar — é montar um objeto de resumo:

js
const pedidos = [
  { numero: 1041, uf: 'SP', total: 289.9 },
  { numero: 1042, uf: 'RJ', total: 1199 },
  { numero: 1043, uf: 'SP', total: 79.5 },
  { numero: 1044, uf: 'MG', total: 399 },
];

const porUf = pedidos.reduce((mapa, pedido) => {
  mapa[pedido.uf] = (mapa[pedido.uf] ?? 0) + pedido.total;
  return mapa;
}, {});

console.log(porUf);
console.log(Object.entries(porUf).map(([uf, v]) => `${uf}: R$ ${v.toFixed(2)}`));
{ SP: 369.4, RJ: 1199, MG: 399 } [ 'SP: R$ 369.40', 'RJ: R$ 1199.00', 'MG: R$ 399.00' ]

Duas linhas fazem o trabalho todo. O ?? 0 cria a chave na primeira vez que a UF aparece; o return mapa é obrigatório, porque sem ele o acumulado da próxima volta seria undefined.

Esquecer esse return é o segundo erro mais comum do reduce, depois do valor inicial — e ele quebra com Cannot read properties of undefined na volta seguinte. É o mesmo diagnóstico do TypeError: Cannot read properties of undefined, vindo de dentro do acumulador.

Quando não usar

reduce tem fama de elegante e por isso é usado onde não deveria:

js
const carrinho = [
  { nome: 'Teclado mecânico', preco: 289.9 },
  { nome: 'Mouse sem fio', preco: 149.9 },
];

const totalComReduce = carrinho.reduce((soma, item) => soma + item.preco, 0);

let totalComFor = 0;
for (const item of carrinho) {
  totalComFor += item.preco;
}

console.log(totalComReduce.toFixed(2), totalComFor.toFixed(2));

const relatorio = carrinho.reduce((texto, item, i) => {
  return `${texto}${i > 0 ? ', ' : ''}${item.nome}`;
}, 'Pedido: ');

console.log(relatorio);
439.80 439.80 Pedido: Teclado mecânico, Mouse sem fio

As duas primeiras somas dão no mesmo. Nesse caso reduce ganha por dizer a intenção — “isto vira um número” — logo na primeira palavra.

O segundo bloco é o contrário: aquele reduce que monta texto seria 'Pedido: ' + carrinho.map((i) => i.nome).join(', '), mais curto e mais legível. Quando o acumulador precisa de um if para saber se é a primeira volta, é sinal de que existe um método mais direto.

Quatro critérios para decidir:

  • Sai uma lista do mesmo tamanho? map.
  • Sai uma sublista, com os itens iguais? filter.
  • Sai um valor só, e a operação é associativa (somar, contar, agrupar)? reduce — com valor inicial.
  • Precisa parar no meio, ou o corpo tem efeito colateral (log, requisição, escrita em tela)? Nenhum dos três: use for...of, como mostra percorrer array em JavaScript.

Com map, filter e reduce você fecha o bloco de arrays da trilha de JavaScript e ganha o vocabulário que aparece em todo código React. Se algum dos exemplos tropeçou num undefined no meio do encadeamento, arrays em JavaScript explica de onde ele veio. O guia completo de JavaScript mostra o que vem depois.

Prefere aprender em vídeo?

Tem aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • arrays
  • map
  • filter
  • reduce
  • encadeamento

Perguntas frequentes

Como escolher entre map, filter e reduce?
Pelo tamanho da saída. Se sai a mesma quantidade de itens, é map. Se sai a mesma quantidade ou menos, sem mudar o conteúdo, é filter. Se sai um valor só — número, string, objeto —, é reduce.
Por que meu map devolveu um array de undefined?
Porque o callback não devolveu nada. Com chaves na arrow function o return é obrigatório; sem chaves, o valor da expressão já é o retorno. map sempre escreve o retorno do callback na posição, e sem return isso é undefined.
reduce é mais rápido que um for?
Não, é um pouco mais lento: cada item custa uma chamada de função. A diferença só importa em laço muito quente. Escolha reduce quando ele deixar a intenção mais clara, não esperando ganho de desempenho.
Devo sempre passar o valor inicial no reduce?
Sim, salvo quando você quer explicitamente o comportamento de comparação entre itens, como pegar o mais caro. Sem valor inicial, array vazio joga TypeError e array de um item devolve o item sem nem chamar o callback.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Array.prototype.reduce() — developer.mozilla.org
  2. MDN — Array.prototype.map() — developer.mozilla.org
  3. ECMAScript 2026 Language Specification — Array.prototype.reduce — tc39.es

Continue por aqui