bateCarimbo.
Código

Como validar CPF e CNPJ em JavaScript e TypeScript (incluindo o CNPJ alfanumérico)

Implementação comentada de validadores de CPF e CNPJ em JavaScript e TypeScript: normalização da entrada, cálculo dos dígitos verificadores, suporte ao CNPJ alfanumérico, testes unitários e as pegadinhas que quebram validadores em produção.

Por · Publicado em · 13 min de leitura

Todo sistema brasileiro acaba precisando de duas funções: validarCpf e validarCnpj. Elas costumam ser copiadas de um gist antigo, funcionam por anos e um dia rejeitam um documento legítimo — ou aceitam um número que ninguém emitiu. Este guia monta as duas do zero em TypeScript, com uma única rotina de módulo 11 compartilhada, suporte ao CNPJ alfanumérico e uma bateria de testes que cobre os casos que costumam passar batido.

Por que regex não basta

Uma expressão regular descreve formato, não validade. Ela sabe dizer que há três grupos de três algarismos, um hífen e mais dois algarismos; não sabe dizer se esses dois últimos algarismos são o resultado correto da conta feita sobre os nove anteriores.

TypeScript
const CPF_MASCARADO = /^\d{3}\.\d{3}\.\d{3}-\d{2}$/;

CPF_MASCARADO.test("123.456.789-09"); // true  — e realmente é válido
CPF_MASCARADO.test("123.456.789-00"); // true  — mas os dígitos estão errados
CPF_MASCARADO.test("111.111.111-11"); // true  — sequência repetida
CPF_MASCARADO.test("12345678909");    // false — válido, só que sem máscara
A regex acerta o formato e erra em tudo que importa.

Os dois últimos casos mostram os dois tipos de erro possíveis. O falso positivo deixa entrar lixo no banco: 111.111.111-11 passa até no cálculo do dígito verificador, porque uma soma ponderada de onze algarismos iguais fecha a conta por acidente. O falso negativo é pior na experiência do usuário: quem digita o documento sem pontuação vê um erro que não existe.

Regex continua útil — para extrair documentos de um texto, para decidir rapidamente se uma string tem qualquer chance de ser um CNPJ, para aplicar máscara. O papel dela é filtrar o formato antes do cálculo, nunca substituí-lo. Os padrões prontos de cada documento estão no guia de regex para documentos brasileiros.

Normalizando a entrada antes de validar

O primeiro passo de qualquer validador é reduzir a entrada a uma forma canônica. Sem isso você acaba escrevendo quatro variações da mesma função — uma para a string com máscara, outra para a sem, outra para a que veio de um CSV com espaço sobrando.

TypeScript
/** Apara espaços, sobe para maiúsculas e descarta tudo que não é 0-9 ou A-Z. */
export function normalizar(entrada: string): string {
  return entrada.trim().toUpperCase().replace(/[^0-9A-Z]/g, "");
}

normalizar("  123.456.789-09  "); // "12345678909"
normalizar("12.abc.345/01de-35"); // "12ABC34501DE35"
normalizar("");                   // ""
Uma normalização serve para CPF e CNPJ, inclusive alfanumérico.

Três decisões estão embutidas nessas três linhas, e vale explicitar cada uma.

Maiúsculas. O CNPJ alfanumérico usa letras de A a Z maiúsculas. Como o valor numérico de cada caractere sai do código ASCII, a letra minúscula produziria um número completamente diferente: a tem código 97, e 97 menos 48 dá 49. Subir para maiúsculas antes de calcular evita que um usuário que digitou em caixa baixa receba um erro.

Espaços. Espaço nas pontas sempre deve ser aparado — é resíduo de copiar e colar, não intenção do usuário. Espaço no meio é uma escolha de produto: a normalização acima os remove, o que aceita 123 456 789 09. Se o seu domínio exige entrada estrita (importação de arquivo de terceiro, por exemplo), valide o formato com regex antes de limpar, e rejeite o que não bater exatamente com a máscara esperada.

String vazia. normalizar("") devolve string vazia, e a validação de tamanho que vem logo depois vai reprovar. Isso é correto, mas não é a mesma coisa que campo obrigatório. Um formulário em que o CPF é opcional precisa tratar vazio como “sem valor” e não como “inválido”, senão o usuário fica travado num campo que ele nem precisaria preencher.

A rotina de módulo 11, escrita uma vez só

CPF e CNPJ usam o mesmo algoritmo: multiplica-se cada caractere da base por um peso, soma-se tudo, tira-se o resto da divisão por 11 e aplica-se a regra final. O que muda entre os dois documentos é apenas a lista de pesos. Então o correto é escrever uma função que recebe a base e os pesos, e deixar cada documento fornecer os seus. A origem da conta está detalhada no guia do módulo 11 e do dígito verificador.

TypeScript
/** Valor numérico do caractere: dígitos viram 0-9; letras seguem ASCII − 48. */
function valorDoCaractere(caractere: string): number {
  return caractere.charCodeAt(0) - 48;
}

/** Soma ponderada da base, resto por 11 e a regra: resto 0 ou 1 → dígito 0. */
export function modulo11(base: string, pesos: number[]): number {
  let soma = 0;
  for (let i = 0; i < base.length; i++) {
    soma += valorDoCaractere(base[i]) * pesos[i];
  }

  const resto = soma % 11;
  return resto < 2 ? 0 : 11 - resto;
}
O coração dos dois validadores: onze linhas que não mudam.

A linha resto < 2 ? 0 : 11 - resto é onde mora o bug mais frequente de todos. Se o resto for 0 ou 1, a subtração daria 11 ou 10 — dois valores que não cabem em um único algarismo. A regra oficial manda usar 0 nesses dois casos. Implementações que escrevem 11 - resto sem a guarda geram dígito 10 e depois tentam salvar com gambiarras do tipo dv % 10, que produz 1 no lugar de 0 e rejeita documentos legítimos.

Validando CPF passo a passo

O CPF tem onze algarismos: nove de base e dois verificadores. O primeiro verificador é calculado sobre a base de nove com pesos de 10 a 2; o segundo, sobre a base de dez (base original mais o primeiro dígito) com pesos de 11 a 2. Os pesos são sempre decrescentes até 2 e nunca reiniciam.

Rejeitar as onze sequências repetidas

Antes de calcular qualquer coisa, elimine 00000000000, 11111111111 e as outras nove. Esses números passam no módulo 11 legitimamente — a matemática não tem como separá-los dos demais — e a Receita não os emite. Aparecem em produção o tempo todo porque são o que alguém digita quando quer pular o campo.

TypeScript
const CPF_REPETIDO = /^(\d)\1{10}$/;

CPF_REPETIDO.test("11111111111"); // true  → rejeitar
CPF_REPETIDO.test("12345678909"); // false → seguir para o cálculo
A referência para trás exige que todos os caracteres sejam iguais ao primeiro.

O grupo (\d) captura o primeiro algarismo e \1{10} exige mais dez ocorrências do mesmo caractere. Uma lista fixa com as onze strings também funciona, mas envelhece pior quando você precisar da mesma ideia para o CNPJ, que tem quatorze posições.

Primeiro dígito verificador

Com a base 123456789, os pesos vão de 10 a 2 na ordem em que os algarismos aparecem:

Primeiro dígito verificador (base 123456789)
CaracterePesoProduto
11010
2918
3824
4728
5630
6530
7428
8324
9218
Soma210

210 ÷ 11 → resto 1

Resto menor que 2, então o dígito é 0.

Dígito verificador = 0

Esse exemplo já exercita a guarda do resto: 210 dividido por 11 deixa resto 1, e um validador sem a condição resto < 2 devolveria 10 aqui.

Segundo dígito verificador

A base agora é 1234567890 — os nove algarismos originais mais o primeiro verificador recém-calculado. Os pesos começam em 11:

Segundo dígito verificador (base 1234567890)
CaracterePesoProduto
11111
21020
3927
4832
5735
6636
7535
8432
9327
020
Soma255

255 ÷ 11 → resto 2

Resto maior ou igual a 2, então o dígito é 11 − 2.

Dígito verificador = 9

O CPF completo é 123.456.789-09. Note que o segundo cálculo depende do primeiro: um erro no dígito inicial se propaga e torna os dois verificadores inválidos, o que é justamente o comportamento desejado.

A função completa em TypeScript

TypeScript
/** 10..2 para uma base de 9; 11..2 para uma base de 10. */
function pesosCpf(tamanho: number): number[] {
  return Array.from({ length: tamanho }, (_, i) => tamanho + 1 - i);
}

const SOMENTE_DIGITOS = /^\d{11}$/;
const CPF_REPETIDO = /^(\d)\1{10}$/;

export function validarCpf(entrada: string): boolean {
  const cpf = normalizar(entrada);

  if (!SOMENTE_DIGITOS.test(cpf)) return false;
  if (CPF_REPETIDO.test(cpf)) return false;

  const base = cpf.slice(0, 9);
  const primeiro = modulo11(base, pesosCpf(9));
  const segundo = modulo11(base + primeiro, pesosCpf(10));

  return cpf.slice(9) === String(primeiro) + String(segundo);
}
Pesos decrescentes gerados a partir do tamanho da base.

A verificação de tamanho e a de caracteres saem de graça na mesma regex: ^\d{11}$ só aceita exatamente onze algarismos, então entradas curtas, longas ou com letra caem fora antes do cálculo. Comparar a string dos dois dígitos calculados com a fatia final é mais simples do que comparar número a número e evita armadilhas de conversão.

Validando CNPJ numérico

O CNPJ tem quatorze caracteres: doze de base e dois verificadores. A diferença em relação ao CPF está nos pesos, que valem de 2 a 9 contados da direita para a esquerda e reiniciam a cada oito posições, porque a base é maior do que a faixa disponível de pesos.

TypeScript
const PESOS_CNPJ = [2, 3, 4, 5, 6, 7, 8, 9];

/** Da direita para a esquerda: 2, 3, 4, ... 9, 2, 3, 4, ... */
function pesosCnpj(tamanho: number): number[] {
  return Array.from(
    { length: tamanho },
    (_, i) => PESOS_CNPJ[(tamanho - 1 - i) % 8],
  );
}

pesosCnpj(12); // [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
pesosCnpj(13); // [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
O módulo 8 é o que faz a sequência de pesos voltar ao início.

Muitas implementações trazem esses dois vetores digitados à mão. Funciona, mas esconde a regra e convida ao erro de copiar o vetor de doze posições para o cálculo do segundo dígito, que precisa de treze. Derivar os pesos do tamanho elimina essa classe de bug.

TypeScript
const CNPJ_FORMATO = /^[0-9A-Z]{12}\d{2}$/;
const CNPJ_REPETIDO = /^(\d)\1{13}$/;

export function validarCnpj(entrada: string): boolean {
  const cnpj = normalizar(entrada);

  if (!CNPJ_FORMATO.test(cnpj)) return false;
  if (CNPJ_REPETIDO.test(cnpj)) return false;

  const base = cnpj.slice(0, 12);
  const primeiro = modulo11(base, pesosCnpj(12));
  const segundo = modulo11(base + primeiro, pesosCnpj(13));

  return cnpj.slice(12) === String(primeiro) + String(segundo);
}
Mesma estrutura do CPF, com outra origem de pesos.

Testando com 11.222.333/0001-81: a base 112223330001 ponderada pelos pesos de 12 posições soma 102, que deixa resto 3 na divisão por 11 e produz o dígito 8; a base estendida soma 120, resto 10, dígito 1. Os verificadores conferem.

Adaptando para o CNPJ alfanumérico

Desde julho de 2026, novas inscrições podem trazer letras nas doze posições da base. Os dois dígitos verificadores continuam numéricos, e a conta continua sendo módulo 11 com os mesmos pesos — o único acréscimo é uma regra de conversão de caractere para número, definida pela Receita Federal: subtrai-se 48 do código ASCII do caractere.

Conversão de caractere para valor (ASCII menos 48)
CaractereCódigo ASCIIValor no cálculo
0480
9579
A6517
B6618
Z9042

A escolha do 48 não é arbitrária: é exatamente o código ASCII do caractere 0. Para os algarismos, portanto, a subtração devolve o próprio valor — "7".charCodeAt(0) - 48 dá 7. É por isso que o mesmo valorDoCaractere serve para os dois formatos e a função validarCnpj escrita acima já aceita letras sem nenhuma alteração. Um CNPJ puramente numérico é apenas o caso particular em que nenhum caractere passa de 9.

O exemplo oficial do SERPRO é 12.ABC.345/01DE-35. Os valores entram como 1, 2, 17, 18, 19, 3, 4, 5, 0, 1, 20 e 21; com os pesos 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3 e 2 a soma dá 459, que deixa resto 8 e produz o dígito 3. Repetindo sobre a base acrescida desse 3, com treze pesos, a soma dá 424, resto 6, dígito 5.

Dois cuidados na migração de um sistema existente. O primeiro é a coluna do banco: CNPJ nunca deveria ter sido bigint, e agora deixou de ser possível. O segundo são as regexes espalhadas pelo código que exigem \d{14} — elas vão rejeitar silenciosamente os documentos novos. O checklist completo está no guia sobre o CNPJ alfanumérico.

Testes: os casos que todo validador deve passar

A tabela abaixo é o mínimo. Cada linha existe porque corresponde a um bug real que já derrubou algum validador em produção.

Casos de teste obrigatórios
EntradaEsperadoO que o caso protege
123.456.789-09válidoCPF com máscara, o formato mais comum em formulário
12345678909válidomesmo CPF sem máscara, vindo de API ou importação
12345678909 válidoespaços nas pontas de copiar e colar
100.000.000-19válidoprimeiro dígito nasce de resto 10
100.000.006-04válidoprimeiro dígito nasce de resto 0
111.111.111-11inválidosequência repetida passa no módulo 11 e precisa de guarda
123.456.789-00inválidoformato perfeito, dígitos verificadores errados
1234567890inválidoum caractere a menos
(vazio)inválidocampo em branco não pode virar exceção
11.222.333/0001-81válidoCNPJ numérico clássico
12.ABC.345/01DE-35válidoCNPJ alfanumérico, exemplo oficial
12abc34501de35válidoletras minúsculas depois da normalização
12ABC34501DEABinválidoos dois verificadores têm de ser numéricos
12345678909inválidoalgarismo Unicode de largura total não é 0-9

O último caso merece atenção. Um usuário com teclado japonês ou um PDF exportado de certos sistemas pode produzir 9 — um caractere que parece nove, mas tem outro ponto de código. Em JavaScript, \d sem a flag ucasa apenas com o intervalo de 0 a 9 do ASCII, então a normalização o descarta e o tamanho passa a não bater. O comportamento correto é rejeitar, e o teste garante que ele não mude por acidente quando alguém resolver “melhorar” a regex.

TypeScript
import { describe, expect, it } from "vitest";

import { validarCnpj, validarCpf } from "./documentos";

describe("validarCpf", () => {
  it.each([
    ["123.456.789-09", true],
    ["12345678909", true],
    ["  12345678909  ", true],
    ["100.000.000-19", true],
    ["100.000.006-04", true],
    ["111.111.111-11", false],
    ["000.000.000-00", false],
    ["123.456.789-00", false],
    ["1234567890", false],
    ["", false],
  ])("%s → %s", (entrada, esperado) => {
    expect(validarCpf(entrada)).toBe(esperado);
  });
});

describe("validarCnpj", () => {
  it.each([
    ["11.222.333/0001-81", true],
    ["12.ABC.345/01DE-35", true],
    ["12abc34501de35", true],
    ["11.222.333/0001-80", false],
    ["00.000.000/0000-00", false],
    ["12ABC34501DEAB", false],
    ["", false],
  ])("%s → %s", (entrada, esperado) => {
    expect(validarCnpj(entrada)).toBe(esperado);
  });
});
Vitest com it.each — a tabela de casos vira a suíte inteira.

Precisando de mais massa do que os exemplos fixos — para teste de carga, para popular um ambiente de homologação, para um teste de propriedade que roda mil documentos —, gere números fictícios com o gerador de CPF e o gerador de CNPJ em vez de recorrer a documentos reais de colegas ou de clientes.

Onde validar em uma aplicação React

Validar a cada tecla digitada é hostil: o campo fica vermelho enquanto o usuário ainda está no terceiro algarismo. O momento certo no cliente é o onBlur, quando ele terminou de preencher e saiu do campo, com o erro limpo assim que ele volta a digitar.

TSX
"use client";

import { useState } from "react";

import { validarCpf } from "@/lib/documentos";

export function CampoCpf() {
  const [valor, setValor] = useState("");
  const [erro, setErro] = useState<string | null>(null);

  function conferir() {
    // Campo vazio é problema de "obrigatório", não de validação.
    if (valor.trim() === "") return setErro(null);
    setErro(validarCpf(valor) ? null : "CPF inválido. Confira os números.");
  }

  return (
    <div>
      <label htmlFor="cpf">CPF</label>
      <input
        id="cpf"
        inputMode="numeric"
        autoComplete="off"
        value={valor}
        onChange={(evento) => {
          setValor(evento.target.value);
          setErro(null);
        }}
        onBlur={conferir}
        aria-invalid={erro !== null}
        aria-describedby={erro ? "cpf-erro" : undefined}
      />
      {erro && (
        <p id="cpf-erro" role="alert">
          {erro}
        </p>
      )}
    </div>
  );
}
Campo controlado que só acusa erro depois que o usuário sai dele.

No envio, revalide antes de chamar a API — o usuário pode ter colado o valor e enviado o formulário com Enter sem nunca disparar o onBlur. E revalide de novo no servidor, sempre. A validação no cliente é ergonomia, não segurança: qualquer pessoa consegue enviar uma requisição direta ao endpoint, e integrações sistema a sistema nem passam pela sua interface. Como a função é pura e não depende de DOM, o mesmo arquivo roda nos dois lados.

TypeScript
import { z } from "zod";

import { validarCnpj, validarCpf } from "@/lib/documentos";

export const cadastroSchema = z.object({
  cpf: z.string().refine(validarCpf, "CPF inválido."),
  cnpjEmpresa: z.string().refine(validarCnpj, "CNPJ inválido."),
});
A mesma função pura reaproveitada no schema do servidor.

Grave o documento normalizado, sem máscara, e aplique a formatação só na exibição. Persistir ora 12345678909, ora 123.456.789-09 transforma qualquer busca por igualdade em loteria e impede um índice único de funcionar.

Bibliotecas prontas e quando usar uma

O ecossistema JavaScript tem pacotes maduros para isso, e usar um é uma escolha defensável. A pergunta útil não é “biblioteca ou código próprio”, e sim quanto do seu domínio o pacote cobre.

Uma dependência de terceiro faz sentido quando você precisa de muito mais do que CPF e CNPJ — inscrição estadual por estado, PIS, título de eleitor, renavam, cada um com sua regra —, quando a equipe é grande e você quer uma única implementação canônica, ou quando não há apetite para manter a regra do resto 10 documentada em algum canto. Cerca de cinquenta linhas sem dependências, por outro lado, custam pouco para manter, não entram no seu orçamento de bundle e não expõem você a um incidente de cadeia de suprimentos por conta de duas funções matemáticas.

Escolhendo um pacote, confira três coisas antes: se ele já suporta o CNPJ alfanumérico (esse é hoje o melhor indicador de manutenção ativa), se ele rejeita as sequências repetidas e se a API aceita entrada com e sem máscara. Um pacote que falha em qualquer um desses pontos vai exigir que você escreva código em volta dele, e aí a economia evaporou.

Erros comuns

Tratar o documento como número. Converter para Number ou gravar em coluna inteira come o zero à esquerda: 012.345.678-90 vira dez algarismos e nunca mais valida. Com o CNPJ alfanumérico o problema deixou de ser sutil e passou a ser impossível de ignorar.

Usar a mesma função de tamanho para os dois documentos. Um campo único que aceita CPF ou CNPJ deve rotear pelo comprimento normalizado: onze caracteres vão para validarCpf, quatorze para validarCnpj, qualquer outro valor é inválido. Tentar as duas funções e aceitar se alguma passar produz mensagens de erro incompreensíveis.

Confundir válido com existente. Um CPF aprovado pelo módulo 11 é apenas matematicamente consistente. Ele pode nunca ter sido emitido, ou estar suspenso, ou pertencer a outra pessoa. Se a sua regra de negócio depende de existência ou de titularidade, é consulta à Receita — assunto do guia CPF válido não é CPF existente.

Exigir máscara na API. A interface pode e deve mascarar; o endpoint não deve recusar 12345678909. Normalize na entrada, aceite os dois formatos e devolva sempre o mesmo.

Não testar a fronteira do resto. Sem um caso de teste com resto 0 e outro com resto 10, a guarda resto < 2 pode ser removida num refactor e ninguém percebe até o primeiro cliente reclamar. A lista completa de armadilhas está no guia de erros comuns em validadores.

Fontes e referências