Converter string em número em JavaScript
Number, parseInt, parseFloat e o unário mais: o que cada um devolve, o radix esquecido e como o NaN silencioso vira um "R$ NaN" na tela do cliente.
Valores vindos de <input>, parâmetros de URL e arquivos CSV normalmente chegam
como strings, mesmo quando mostram dígitos. Conversão numérica é a operação
que transforma esse texto em um valor do tipo number antes de calcular. O
JavaScript oferece funções parecidas, mas elas não validam a entrada da mesma
forma.
Imagine uma etiqueta entregue ao caixa. Number só aceita a etiqueta inteira
como um número válido; parseFloat começa pela esquerda e para ao encontrar um
caractere que não participa da leitura; parseInt faz essa leitura para inteiro.
No código, isso explica por que '12abc' vira NaN com Number e 12 com os
parsers. Escolher um deles também é escolher sua regra de validação.
Use Number quando todo o texto precisa ser válido. Use parseFloat apenas
quando aproveitar um prefixo é realmente desejado, parseInt para inteiro com
radix explícito e trate o + unário como a forma curta — e menos legível — de
Number.
const digitado = '289.90';
console.log(Number(digitado));
console.log(parseInt(digitado, 10));
console.log(parseFloat(digitado));
console.log(+digitado);Repare que parseInt já cortou os centavos. Com uma entrada bem comportada,
três das quatro concordam. O interessante começa quando a entrada não é bem
comportada — que é sempre.
Number é rígida, e transforma vazio em zero
console.log(Number('12abc'));
console.log(Number(''));
console.log(Number(' '));
console.log(Number(null));
console.log(Number(undefined));
console.log(Number(true));Duas informações valem ouro aí.
A primeira: Number('12abc') é NaN. Ou a string inteira é um número, ou não é
número nenhum. Não existe meio-termo, e é isso que torna Number a escolha certa
para validar entrada.
A segunda, e essa derruba gente todo mês: Number('') é zero. Campo de
formulário em branco não vira NaN, vira 0. Se a sua validação é
if (Number.isNaN(valor)), o campo vazio passa direto e o pedido é criado com
quantidade zero. Number(null) também é 0, pelo mesmo motivo; só
Number(undefined) devolve NaN.
parseInt lê até o primeiro caractere inválido
parseInt tem outra filosofia: ele varre a string da esquerda para a direita e
para no primeiro caractere que não serve, devolvendo o que já leu.
console.log(parseInt('12abc', 10));
console.log(parseInt(' 42 unidades', 10));
console.log(parseInt('abc12', 10));
console.log(parseInt('', 10));
console.log(parseInt('289.90', 10));Ele pula espaço em branco no começo, aceita lixo no fim e desiste quando o lixo
vem primeiro. A última linha é a que mais custa caro: o ponto decimal não é um
caractere válido para inteiro, então parseInt('289.90') devolve 289 e joga os
noventa centavos fora, sem avisar.
Note também que parseInt('') é NaN, enquanto Number('') é 0. Duas funções
da mesma linguagem discordando sobre o significado de string vazia.
O radix, e o mapa que devolve NaN no meio
O segundo argumento do parseInt é a base numérica. Ele é opcional, e essa
opcionalidade já foi um dos bugs mais famosos do JavaScript:
console.log(parseInt('08'));
console.log(parseInt('08', 10));
console.log(parseInt('0x1f'));
console.log(parseInt('11', 2));
console.log(['1', '7', '11'].map(parseInt));Antes do ES5, uma string começada em 0 era interpretada como octal — e
parseInt('08') devolvia 0, porque 8 não existe na base 8. Um campo de mês ou
de dia com zero à esquerda virava zero. Hoje esse comportamento acabou: as duas
primeiras linhas dão 8.
O prefixo 0x, porém, continua valendo: parseInt('0x1f') lê hexadecimal e
devolve 31. E a base explícita funciona — parseInt('11', 2) lê binário e
devolve 3.
A última linha é o motivo real de sempre passar o radix. map chama a função com
três argumentos: valor, índice e array. O índice vai parar no lugar do radix.
Então a conversão foi parseInt('1', 0), parseInt('7', 1) e
parseInt('11', 2) — base 1 não existe, daí o NaN no meio. Quem escreve
.map(Number) não tem esse problema, porque Number ignora os argumentos extras.
parseFloat e a vírgula brasileira
parseFloat é o parseInt com casas decimais: mesma tolerância, mesmo “leia até
onde der”. E aqui aparece o problema que ninguém escapa no Brasil:
console.log(parseFloat('289,90'));
console.log(Number('289,90'));
console.log(parseFloat('1.234,56'));
console.log(Number('1.234,56'));Nenhuma das duas entende vírgula decimal. O parseFloat para na vírgula e devolve
289 — perdendo os centavos em silêncio. Pior: em '1.234,56' ele lê o ponto
como separador decimal e devolve 1.234, transformando mil duzentos e trinta e
quatro reais em um real e vinte e três centavos. O Number pelo menos falha
alto, devolvendo NaN.
Não existe função nativa que leia o formato brasileiro. Você precisa normalizar o texto antes — é o que a última seção resolve.
O unário + é o Number disfarçado
Colocar um + na frente de um valor força a conversão numérica, com exatamente as
mesmas regras do Number:
console.log(+'289.90');
console.log(+'');
console.log(+[]);
console.log(+['7']);
console.log(+{});
console.log(+'1e3');As linhas com colchete e chave explicam por que ele é controverso. Array vazio
vira 0, array de um elemento vira aquele elemento convertido, objeto vira
NaN — tudo consequência da conversão para string antes da conversão para
número.
É conciso e é idiomático em código de biblioteca. Em código de time, Number(x)
diz o que faz sem ninguém precisar saber essas regras.
Erros comuns
O erro característico deste assunto não é uma exceção: é o NaN que ninguém
percebeu.
const formulario = { quantidade: '2', precoUnitario: 'R$ 289,90' };
const total = Number(formulario.precoUnitario) * Number(formulario.quantidade);
console.log(`Total: R$ ${total.toFixed(2)}`);Nenhum erro foi lançado. toFixed funcionou normalmente — NaN é number, e
number tem toFixed. O resultado foi para a tela do cliente escrito por
extenso: R$ NaN. Todo desenvolvedor front-end já viu isso em produção pelo
menos uma vez.
Quando ele finalmente derruba o programa, é longe da causa:
const faixasDeFrete = [12.5, 18.9, 27.4];
const peso = Number('2,5');
const faixa = faixasDeFrete[Math.floor(peso)];
console.log(faixa.toFixed(2));TypeError: Cannot read properties of undefined (reading ‘toFixed’) at file:///private/tmp/loja/l7-propagacao.mjs:6:19 Node.js v24.16.0
Siga a cadeia de trás para frente: faixa é undefined porque o índice era
NaN; o índice era NaN porque Math.floor(NaN) é NaN; e o peso era NaN
porque '2,5' tem vírgula. O erro aponta a linha 6, e o defeito está na linha 3.
É a mesma leitura descrita em
Cannot read properties of undefined:
a mensagem indica onde estourou, não onde nasceu. Quando um NaN aparecer,
procure a primeira conversão da cadeia.
Vale ver as quatro funções discordando lado a lado:
const entradas = ['289.90', '289,90', 'R$ 1.234,56', '', ' ', '12abc'];
for (const entrada of entradas) {
console.log(
JSON.stringify(entrada).padEnd(14),
'Number=' + String(Number(entrada)).padEnd(8),
'parseFloat=' + String(parseFloat(entrada)).padEnd(8),
'parseInt=' + parseInt(entrada, 10),
);
}Seis entradas, e em quatro delas as funções discordam. Escolher pela conveniência do momento é como o mesmo formulário passa a aceitar valores diferentes em telas diferentes.
Um conversor seguro para formulário
A regra é converter na borda — no instante em que o dado entra no sistema — e
devolver null em vez de NaN, porque null não sobrevive a uma conta sem
alguém perceber:
function paraNumero(texto) {
const limpo = String(texto)
.replace(/[^\d,.-]/g, '')
.replace(/\./g, '')
.replace(',', '.');
if (limpo === '') return null;
const numero = Number(limpo);
return Number.isNaN(numero) ? null : numero;
}
console.log(paraNumero('R$ 1.234,56'));
console.log(paraNumero('289,90'));
console.log(paraNumero('grátis'));
console.log(paraNumero(''));Três passos, na ordem: remove tudo que não é dígito, vírgula, ponto ou sinal;
apaga os pontos de milhar; troca a vírgula decimal por ponto. O if antes do
Number existe justamente por causa do Number('') === 0 que vimos lá em cima —
sem ele, 'grátis' viraria zero.
Ele é deliberadamente brasileiro, e isso tem preço: como apaga todo ponto,
paraNumero('289.90') no formato americano devolve 28990. Decida qual formato
o campo aceita antes de converter — um conversor que tenta adivinhar os dois é
como o erro de cem vezes o valor entra no pedido.
Para o caminho inverso, exibir o número no formato brasileiro, o próprio JavaScript resolve:
const total = 1234.56;
console.log(total.toFixed(2));
console.log(total.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' }));Converter na entrada, calcular sempre com número, formatar só na saída. Esses
três hábitos eliminam o R$ NaN do seu projeto de uma vez.
Teste o conversor com uma tabela de casos: '0', '', ' ', '12abc',
'289,90' e 'R$ 1.234,56'. Para cada entrada, escreva antes o resultado
esperado e depois compare com a função. A prática termina quando vazio e texto
inválido viram null, zero continua sendo 0 e o preço brasileiro vira
1234.56.
Com isso o bloco de fundamentos da trilha de JavaScript está fechado: você já sabe rodar código, imprimir, guardar valores, reconhecer tipos e converter entre eles. O próximo salto são funções, onde essa lógica passa a ter nome e a ser reaproveitada — e o guia de JavaScript mostra o que vem depois delas.
Perguntas frequentes
Qual a diferença entre Number e parseInt?
Ainda preciso passar o radix no parseInt?
Como converter um preço com vírgula, tipo "1.234,56"?
Por que Number("") devolve 0 e não NaN?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — Number() — developer.mozilla.org
- MDN — parseInt() — developer.mozilla.org
- ECMAScript — ToNumber Applied to the String Type — tc39.es


