7 Erros de TypeScript que Destroem sua Tipagem Sem Você Perceber
← Voltar para Codeshort

7 Erros de TypeScript que Destroem sua Tipagem Sem Você Perceber

any, type assertions sem verificação, inferência ignorada — os erros que todo dev comete nos primeiros meses com TypeScript e que custam caro lá na frente.

DC
Dev Code Software
13 de abril de 2026·10 min de leitura

Índice


Segundo o State of JS 2024, TypeScript é usado por mais de 78% dos desenvolvedores JavaScript. Mas há uma diferença enorme entre usar TypeScript e usar TypeScript de forma que realmente protege seu código.

A versão silenciosa do problema: você configura o TypeScript, o projeto compila, o VS Code para de sublinhar seus erros — e você acha que está seguro. Não está. Boa parte do código que passa na compilação não está sendo protegido de verdade. Está apenas silenciando os erros de uma forma que o compilador aceita.

Este artigo cobre os sete padrões mais comuns que transformam TypeScript em um teatro de tipagem: código que parece protegido, compila sem reclamar — e quebra em runtime exatamente como quebraria em JavaScript puro.

Se você já olhou para um TypeError: Cannot read properties of null em produção num projeto TypeScript e pensou "mas como isso passou?", esse artigo é a resposta.


TypeScript decorativo: quando o compilador compila mas não protege

Tem uma versão do TypeScript que não protege nada. Ela compila sem erros, o VS Code não reclama, e o runtime quebra do mesmo jeito que quebraria em JavaScript puro.

// Esse código compila limpo — e quebra em runtime
function processarPedido(pedido: any) {
  return pedido.itens.reduce((acc: any, item: any) => acc + item.preco, 0);
}

processarPedido(null); // TypeError: Cannot read properties of null

Se você leu any três vezes nesse snippet e achou normal, esse artigo é pra você.

O TypeScript te dá exatamente o que você pediu. Se você pede menos proteção — com any, assertions cegas, e opcionais em tudo — é exatamente isso que você recebe. O compilador não vai te salvar de você mesmo.


Diagnóstico rápido: você tem TypeScript decorativo?

Antes de mergulhar nos erros, faça o diagnóstico em 30 segundos. Abra qualquer arquivo do seu projeto e responda:

  • Você tem strict: false (ou sem strict) no tsconfig.json?
  • Você usa any pelo menos uma vez por arquivo em média?
  • Você usa as AlgumTipo em retornos de fetch ou JSON.parse?
  • Você adiciona ! (non-null assertion) para o compilador parar de reclamar?
  • Seus catch(e) acessam e.message diretamente sem verificar instanceof Error?

Se você respondeu "sim" para dois ou mais, continue lendo — há brechas reais na sua proteção de tipos. Cada item acima corresponde a um dos sete erros abordados abaixo.


any: o buraco no sistema de tipos

any desliga a verificação de tipos completamente. Uma variável do tipo any aceita qualquer atribuição, qualquer acesso de propriedade, qualquer chamada de método — sem erro, sem aviso.

// Tudo isso compila sem reclamar
let dado: any = { nome: "Ana" };
dado = 42;
dado = null;
console.log(dado.nome.toUpperCase()); // TypeError em runtime se dado for null

O problema não é usar any uma vez. É o padrão que começa quando você tipa o retorno de fetch como any porque não sabe o formato, ou quando tipifica o parâmetro como any para o TypeScript parar de reclamar. A partir daí, any se espalha porque any é contagioso: qualquer coisa que recebe um any vira any também.

// O tipo se espalha silenciosamente
function buscarUsuario(id: string): any {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

const usuario = await buscarUsuario("123");
// usuario: any
// usuario.email: any
// usuario.email.toLowerCase(): any — sem erro, mesmo que email não exista

O que usar no lugar:

// Tipo explícito para o retorno esperado
interface Usuario {
  id: string;
  nome: string;
  email: string;
}

async function buscarUsuario(id: string): Promise<Usuario> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Usuario;
}

any vs unknown: a diferença que muda tudo

Comportamentoanyunknown
Aceita qualquer valor✅ sim✅ sim
Permite acesso a propriedades sem verificação✅ sim (sem erro)❌ não (erro de compilação)
Exige narrowing antes de usar❌ não✅ sim
Contaminação de tipo✅ se espalha❌ contida
Indicado paraRaramente (código de baixo nível)catch, dados externos, retornos genéricos

Na prática: troque any por unknown sempre que você genuinamente não souber o tipo. O compilador vai te forçar a verificar antes de usar — que é exatamente o comportamento que você queria do TypeScript desde o início.

Quando any é legítimo: quase nunca em código de aplicação. Em código de utilitário genérico de baixo nível — onde unknown ou generics não encaixam — pode fazer sentido, mas são casos raros. Se você está usando any para o compilador parar de reclamar, esse é um sinal de que precisa entender melhor o tipo real, não silenciar o aviso.

⚠️ Atenção: ative "noImplicitAny": true no tsconfig.json. Sem essa flag, o TypeScript silenciosamente infere any em vários contextos — parâmetros de função sem tipo, variáveis inicializadas sem valor, callbacks em forEach sem anotação. Com ela ativa, o compilador força você a ser explícito. Isso vai parecer chatice nas primeiras semanas. É exatamente o ponto.

{
  "compilerOptions": {
    "strict": true
  }
}

Type assertions sem verificação real

as diz ao compilador: "eu sei que esse valor é desse tipo, pode confiar em mim". O compilador confia. Se você estiver errado, descobre em runtime.

// Assertion cega — compila, quebra em runtime
const resposta = await fetch("/api/config");
const config = await resposta.json() as ConfigApp;

console.log(config.timeout.toString()); // TypeError se timeout vier como string ou undefined

O uso correto de as é para situações onde você tem informação que o compilador não tem acesso — não para silenciar erros que o compilador está certo em apontar.

O erro mais frequente em PRs: usar as para converter tipos incompatíveis em vez de entender por que os tipos não batem.

// Errado: forçando tipos incompatíveis
const elemento = document.getElementById("btn") as HTMLButtonElement;
elemento.disabled = true; // quebra se o elemento não existir ou não for button
// Certo: verificar antes de assumir
const elemento = document.getElementById("btn");
if (elemento instanceof HTMLButtonElement) {
  elemento.disabled = true; // TypeScript sabe que é HTMLButtonElement aqui
}

Validação real de dados externos com Zod

Para dados externos (API, JSON.parse, localStorage): use uma biblioteca de validação. O as não valida — apenas silencia. O zod faz as duas coisas:

import { z } from "zod";

const ConfigSchema = z.object({
  timeout: z.number(),
  baseUrl: z.string().url(),
  debug: z.boolean().default(false),
});

type Config = z.infer<typeof ConfigSchema>;

const config = ConfigSchema.parse(await resposta.json());
// Se o JSON não bater com o schema: ZodError em runtime — explícito, tratável
// Se bater: config é tipado corretamente — garantia real, não promessa

💡 Dica: use as apenas quando você realmente sabe mais que o compilador — narrowing manual, integração com API que retorna tipo genérico, ou código de interop. Se você está usando as para o erro sumir, o problema não foi resolvido.


Ignorar inferência e tipar tudo na mão

O TypeScript tem inferência de tipos excelente. Em muitos contextos, anotar o tipo explicitamente é redundante — e às vezes atrapalha.

// Redundante — TypeScript já infere isso
const nome: string = "Ana";
const idade: number = 28;
const ativo: boolean = true;
const ids: number[] = [1, 2, 3];

// Deixe o TypeScript inferir — o tipo é igualmente seguro
const nome = "Ana";
const idade = 28;
const ativo = true;
const ids = [1, 2, 3];

Onde a redundância tem custo real: quando você anota manualmente com um tipo mais amplo do que o necessário, e perde a precisão que a inferência daria.

// Você perdeu a informação de que status é especificamente uma dessas strings
const status: string = "ativo";

// TypeScript infere: status: "ativo" — tipo literal, mais preciso
const status = "ativo";

// Para objetos que devem ser constantes, as const vai ainda mais longe
const STATUS = {
  ATIVO: "ativo",
  INATIVO: "inativo",
  PENDENTE: "pendente",
} as const;

type StatusUsuario = typeof STATUS[keyof typeof STATUS];
// StatusUsuario: "ativo" | "inativo" | "pendente"

Quando anotar explicitamente faz sentido

  • Parâmetros de função — inferência não funciona em parâmetros, TypeScript precisa que você declare
  • Retorno de função pública — documentação e contrato explícito para quem consome
  • Variáveis que serão atribuídas depois — let usuario: Usuario | null = null
  • Quando a inferência retorna um tipo mais amplo do que você quer
// Parâmetros sempre anotados — TypeScript não tem como inferir de fora
function calcularDesconto(preco: number, percentual: number): number {
  return preco * (1 - percentual / 100);
}

// Retorno explícito em funções públicas — contrato claro
async function buscarPedidos(userId: string): Promise<Pedido[]> {
  // se você mudar a implementação e o retorno real deixar de bater com Promise<Pedido[]>,
  // o TypeScript vai reclamar aqui — não no código que consome a função
}

Qual a diferença entre interface e type no TypeScript e quando usar cada um?

Essa pergunta aparece em todo onboarding de TypeScript. A resposta honesta é que para a maioria dos casos, são intercambiáveis. Mas as diferenças existem e importam em contextos específicos.

// interface — pode ser estendida depois (declaration merging)
interface Usuario {
  id: string;
  nome: string;
}

interface Usuario {
  email: string; // válido — funde com a declaração anterior
}

// type — não pode ser redeclarado
type Produto = {
  id: string;
  nome: string;
};

// type Produto = { preco: number }; // Erro: Duplicate identifier 'Produto'

O declaration merging de interfaces é usado por bibliotecas para permitir que você estenda tipos existentes — Request do Express, por exemplo. Se você está construindo uma biblioteca ou um módulo com contratos extensíveis, interface é a escolha.

Diferenças práticas para código de aplicação

// type permite union e intersections que interface não suporta
type Resultado<T> = { sucesso: true; dados: T } | { sucesso: false; erro: string };

type Admin = Usuario & { permissoes: string[] }; // intersection

// interface é melhor para objetos com herança
interface Animal {
  nome: string;
}

interface Cachorro extends Animal {
  raca: string;
}

A regra simples que funciona para a maioria:

  • Objetos que representam entidades do domínio (usuário, pedido, produto): interface
  • Unions, intersections, tipos utilitários, aliases de primitivos: type
  • Em caso de dúvida: escolha um padrão para o projeto e seja consistente

O que não fazer é misturar aleatoriamente sem critério — isso cria inconsistência que complica o onboarding de novos devs no projeto.


Tipos opcionais usados errado

O operador ? em TypeScript marca uma propriedade como T | undefined. É poderoso — e fácil de usar errado de duas formas opostas.

Problema 1: opcional em tudo para o compilador parar de reclamar

// Todo campo opcional — o tipo não diz nada útil
interface Pedido {
  id?: string;
  itens?: ItemPedido[];
  total?: number;
  criadoEm?: Date;
}

function calcularTotal(pedido: Pedido) {
  return pedido.itens?.reduce((acc, item) => acc + item.preco, 0); // number | undefined
  // quem consome precisa tratar undefined — mas esse undefined nunca deveria existir
}

Se itens é obrigatório para um Pedido existir, não deve ser opcional. O TypeScript não vai te ajudar a descobrir isso — você precisa pensar sobre o que faz sentido no domínio.

Problema 2: não usar optional chaining quando deveria

// TypeScript avisa mas o dev ignora
interface Config {
  timeout?: number;
}

function aplicarConfig(config: Config) {
  setTimeout(() => executar(), config.timeout); // Tipo 'number | undefined' não é 'number'
  // Muitos devs adicionam um `as number` aqui — solução errada
}

// Trate o undefined explicitamente
function aplicarConfig(config: Config) {
  setTimeout(() => executar(), config.timeout ?? 5000);
}

💡 Dica: o operador ?? (nullish coalescing) retorna o operando direito quando o esquerdo é null ou undefined — diferente do || que também ativa para 0 e "". Para valores numéricos e strings que podem ser zero ou vazios, sempre use ??.

// || ignora 0 e "" — comportamento errado para números e strings
const timeout = config.timeout || 5000; // se timeout for 0, usa 5000

// ?? só ignora null e undefined
const timeout = config.timeout ?? 5000; // se timeout for 0, usa 0

Non-null assertion (!) tem o mesmo problema do as

// Você está prometendo que isso nunca é null — o compilador acredita
const btn = document.getElementById("meu-btn")!;
btn.addEventListener("click", handler); // TypeError se o elemento não existir

Use ! apenas quando você tem certeza absoluta e verificou — não como atalho para o compilador parar de reclamar.


Erros em funções assíncronas e catch sem tipo

Em TypeScript, o parâmetro de catch tem tipo unknown por padrão (com useUnknownInCatchVariables, que faz parte do strict). Muitos devs reagem a isso desligando a flag ou adicionando as Error sem verificar.

// Assumption perigosa — nem todo erro é instância de Error
try {
  await operacaoAssincrona();
} catch (e) {
  console.error((e as Error).message); // pode quebrar se alguém fizer: throw "string de erro"
}
// Verificar antes de acessar
try {
  await operacaoAssincrona();
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  } else {
    console.error("Erro desconhecido:", e);
  }
}

Utilitário para reutilizar em todo o projeto

Para projetos com muitas operações assíncronas, um utilitário pequeno elimina a repetição:

function isError(e: unknown): e is Error {
  return e instanceof Error;
}

function mensagemDeErro(e: unknown): string {
  if (isError(e)) return e.message;
  if (typeof e === "string") return e;
  return "Erro desconhecido";
}

try {
  await operacaoAssincrona();
} catch (e) {
  logger.error(mensagemDeErro(e));
}

Retorno explícito em funções assíncronas

// Retorno implícito vira Promise<Usuario | undefined | null | ...>
async function buscarOuCriarUsuario(email: string) {
  const existente = await db.user.findUnique({ where: { email } });
  if (existente) return existente;
  return db.user.create({ data: { email } });
}

// Retorno explícito — contrato claro
async function buscarOuCriarUsuario(email: string): Promise<Usuario> {
  const existente = await db.user.findUnique({ where: { email } });
  if (existente) return existente;
  return db.user.create({ data: { email } });
}

Checklist: TypeScript que realmente protege

Use isso como referência antes de abrir um PR ou revisar código novo:

ItemO que verificar
✅ strict: true no tsconfignoImplicitAny, strictNullChecks e mais estão ativados?
✅ Zero any no código de aplicaçãoSe tem any, é intencional e documentado?
✅ as com verificação realType assertions têm validação antes ou usam instanceof?
✅ Dados externos validadosRetorno de fetch, JSON.parse, localStorage passa por schema (zod, valibot)?
✅ Inferência aproveitadaVariáveis locais sem anotação redundante?
✅ Retornos de função pública tipadosFunções exportadas têm tipo de retorno explícito?
✅ Opcionais com razãoCampos opcionais em interfaces são realmente opcionais no domínio?
✅ ?? no lugar de || para números/stringsValores 0 e "" não são tratados como ausentes?
✅ catch trata unknownParâmetros de catch verificam instanceof Error antes de acessar .message?
✅ Non-null assertion com certeza! só onde você verificou ou tem garantia do DOM?

FAQ

Quando usar inferência de tipo e quando anotar explicitamente no TypeScript?

Deixe o TypeScript inferir onde ele consegue fazer isso corretamente — inicializações diretas (const x = 42), retornos de funções curtas, variáveis locais evidentes. Anote explicitamente em parâmetros de função (sempre), retornos de funções públicas, e variáveis que serão atribuídas depois ou que precisam de um tipo mais restrito do que a inferência daria. A regra não é "anote tudo" nem "nunca anote" — é "anote onde agrega informação".

Qual a diferença entre interface e type no TypeScript e quando usar cada um?

Para objetos do domínio e contratos de API, use interface — é a convenção mais comum e suporta extensão via declaration merging. Para unions, intersections, tipos condicionais e aliases de primitivos, use type. Em projetos novos, o mais importante é escolher um critério e manter consistente. O TypeScript em si não impõe uma escolha — mas o código de uma base inteira misturando os dois sem critério é desnecessariamente confuso.

TypeScript any vs unknown: qual a diferença e por que unknown é mais seguro?

any desabilita a verificação de tipos completamente — você pode fazer qualquer coisa com um any sem erro. unknown é o oposto seguro: você pode atribuir qualquer coisa a unknown, mas para fazer qualquer operação precisa antes narrowar o tipo (com typeof, instanceof, ou type guard). Use unknown para valores cujo tipo você genuinamente não sabe — parâmetros de catch, dados externos, retornos genéricos. Use any raramente e com intenção.

Por que o TypeScript tipifica o parâmetro catch como unknown e como tratar corretamente?

Com strict: true (ou useUnknownInCatchVariables: true diretamente), o TypeScript trata o parâmetro de catch como unknown — porque qualquer coisa pode ser jogada com throw, não só instâncias de Error. A solução correta é verificar com instanceof Error antes de acessar .message. A solução errada é adicionar as Error sem verificar ou desligar a flag.

Como evitar que any se espalhe pelo código?

Ative "noImplicitAny": true e configure o ESLint com a regra @typescript-eslint/no-explicit-any como warn ou error. Isso não proíbe any completamente — proíbe o uso silencioso. Se você precisar usar any, precisa fazer isso de forma explícita e o lint vai sinalizar para revisão. Para dados externos, use zod ou valibot na borda — isso cria uma fronteira onde unknown entra e tipo validado sai, sem que any precise aparecer no meio.