Versão tipada do algoritmo, com um type guard e um tipo Cnpj de marca (branded type) para você não confundir uma string qualquer com um CNPJ já validado.
0=0 … 9=9, A=17 … Z=42), os pesos vão de 2 a 9 da direita para a esquerda, soma-se tudo, divide-se por 11 e o dígito é 0 quando o resto é menor que 2, ou 11 menos o resto. Ver a conta detalhada.Código completo
export type Cnpj = string & { readonly __marca: 'cnpj' };
const NAO_ALFANUMERICO = /[^0-9A-Z]/g;
const FORMATO = /^[0-9A-Z]{12}\d{2}$/;
const REPETIDO = /^(.)\1{13}$/;
export function limparCnpj(valor: string): string {
return (valor ?? '').toUpperCase().replace(NAO_ALFANUMERICO, '');
}
function digito(sequencia: string): number {
let soma = 0;
for (let i = 0; i < sequencia.length; i += 1) {
const valor = sequencia.charCodeAt(i) - 48;
const peso = ((sequencia.length - 1 - i) % 8) + 2;
soma += valor * peso;
}
const resto = soma % 11;
return resto < 2 ? 0 : 11 - resto;
}
export function calcularDv(base: string): string {
const primeiro = digito(base);
const segundo = digito(`${base}${primeiro}`);
return `${primeiro}${segundo}`;
}
/** Type guard: estreita o tipo para Cnpj quando a validação passa. */
export function ehCnpj(valor: string): valor is Cnpj {
const cnpj = limparCnpj(valor);
if (!FORMATO.test(cnpj) || REPETIDO.test(cnpj)) return false;
return calcularDv(cnpj.slice(0, 12)) === cnpj.slice(12);
}
export function paraCnpj(valor: string): Cnpj {
const cnpj = limparCnpj(valor);
if (!ehCnpj(cnpj)) throw new TypeError(`CNPJ inválido: ${valor}`);
return cnpj;
}
Testes e integração com o framework
// Zod
import { z } from 'zod';
import { ehCnpj } from './cnpj';
export const esquemaEmpresa = z.object({
razaoSocial: z.string().min(3),
cnpj: z.string().refine(ehCnpj, { message: 'CNPJ inválido' }),
});
// class-validator
import { registerDecorator, ValidationOptions } from 'class-validator';
export function EhCnpj(opcoes?: ValidationOptions) {
return (alvo: object, propriedade: string) => {
registerDecorator({
name: 'ehCnpj',
target: alvo.constructor,
propertyName: propriedade,
options: opcoes,
validator: {
validate: (valor: unknown) => typeof valor === 'string' && ehCnpj(valor),
defaultMessage: () => 'CNPJ inválido',
},
});
};
}
Detalhes que costumam quebrar em TypeScript
- O branded type evita o bug clássico de passar um CNPJ não validado adiante — o compilador cobra a validação.
- Guarde sempre a versão limpa (14 caracteres, sem máscara) no domínio e formate só na camada de apresentação.
Casos de teste recomendados
Cubra pelo menos estes cenários — eles pegam praticamente todos os erros de implementação:
| Entrada | Esperado | O que testa |
|---|---|---|
12.ABC.345/01DE-35 | válido | exemplo oficial, com máscara |
12abc34501de35 | válido | normalização para maiúsculas |
11.222.333/0001-81 | válido | compatibilidade com o formato numérico |
11.222.333/0001-82 | inválido | dígito verificador errado |
12ABC34501DEAB | inválido | letra no dígito verificador |
12ABC34501DE3 | inválido | tamanho incorreto |
00.000.000/0000-00 | inválido | sequência repetida |
"" / null | inválido | entrada vazia sem exceção |
Precisa de mais massa de teste? O gerador produz até 500 CNPJs alfanuméricos válidos de uma vez, com opção de baixar em .txt.