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 depoislet 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.