Erros Comuns no TypeScript: 15 que Você Deve Evitar

Erros Comuns no TypeScript: 15 que Você Deve Evitar





Erros Comuns em TypeScript que Você Deve Evitar | yurideveloper.com.br

Boas práticas em TypeScript

Erros comuns em TypeScript que você deve evitar

TypeScript não é só “tipar”: é fazer o compilador te ajudar a encontrar inconsistências cedo.
Abaixo estão os deslizes mais frequentes (e perigosos) que eu vejo em projetos reais — com alternativas melhores.



Tipagem que “ment” pro compilador



Narrowing e união discriminada



Segurança em tempo de execução

1) Usar any como “atalho”

O erro mais comum é abrir mão do sistema de tipos. Quando você usa any, você desliga
justamente a parte do TypeScript que previne erros de integração, mudanças de contrato e bugs silenciosos.

Regra prática

Não “proteja” tipo com any. Troque por tipos reais:
interfaces, uniões e tipos auxiliares. Se estiver difícil, é sinal de que o contrato precisa ficar claro.

O que evitar:

  • function parse(data: any) sem validação e sem contrato de retorno.
  • catch (e) { return e } com e: any e retorno inconsistente.
  • “Destravar” erros com casts no lugar de corrigir o modelo de dados.

Sugestão: prefira tipos parametrizados e tipos de domínio (DTOs) com campos e invariantes explícitas.

2) Confiar em casts (as) sem checagem

Outro erro frequente é usar as para “convencer” o compilador. Isso é perigoso porque o compilador
passa a aceitar valores que podem não existir em runtime (ex.: JSON vindo de API, eventos, storage).

Se é externo (rede, arquivo, localStorage, worker, user input), o tipo precisa ser provado.

Diagnóstico rápido

Se você vê x as Tipo em pontos onde os dados ainda não foram verificados,
é provável que você esteja só adiando o bug para o runtime.

O melhor caminho é:

  • Checar em runtime com type guards quando o dado entra no sistema.
  • Modelar com uniões (por exemplo, eventos discriminados) em vez de forçar casts.
  • Usar casts apenas em cenários seguros (ex.: valores internos que já foram normalizados).

3) Falhar no narrowing: ignorar uniões e discriminantes

TypeScript brilha quando você descreve estados diferentes com uniões e faz o narrowing funcionar.
Quando você não usa isso, você acaba com código cheio de if frágil ou com propriedades “possivelmente inexistentes”.

O erro comum aqui é tratar tudo como um único formato, mesmo quando o domínio tem variações.
Use discriminated unions para que o compilador entenda o que existe em cada caso.

Por que isso importa?

Sem discriminante, você perde o benefício do compilador para acessar campos corretos.
Com discriminante, o TypeScript faz narrowing automaticamente dentro de cada ramo.

Checklist do que evitar:

  • “Um objeto com vários campos opcionais” onde na prática existem estados mutuamente exclusivos.
  • Negligenciar in e checagens por campo discriminante.
  • Dependência excessiva de ?. e as para contornar ausência de tipo.

4) Ignorar validação e “assumir” invariantes

Tipos são verificados em tempo de compilação. Mas seu código roda em runtime — e dados reais sempre chegam
com variações. Se você depende de invariantes (ex.: “campo X sempre existe”), valide na entrada.

Um erro comum é modelar funções como se elas fossem recebendo valores sempre válidos, mas na prática
elas recebem desde undefined até strings vazias e números fora do intervalo.

Estratégia simples

Confirme a forma do dado ao entrar (boundary) e mantenha o resto do código trabalhando com tipos
já “sanitizados”.

Impactos típicos de não validar:

  • Falhas em cascata: um valor inválido vira NaN, depois vira erro em renderização.
  • Estados impossíveis no domínio que passam a existir (ex.: status inválido).
  • Tratamento reativo: corrigir erros um por um sem uma base consistente de tipos.

Código exemplo: type guard + união discriminada (sem casts perigosos)

Vamos supor que você recebe um evento e precisa processar corretamente conforme o type.
Abaixo, eu valido a entrada e uso um discriminante para o TypeScript fazer narrowing com segurança.

type UserEvent =
  | { type: 'user:created'; userId: string; email: string }
  | { type: 'user:updatedEmail'; userId: string; email: string }
  | { type: 'user:deleted'; userId: string };

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

function isUserEvent(value: unknown): value is UserEvent {
  if (!isRecord(value)) return false;

  const type = value.type;
  const userId = value.userId;
  const email = value.email;

  if (typeof type !== 'string' || typeof userId !== 'string') return false;

  switch (type) {
    case 'user:created':
    case 'user:updatedEmail':
      return typeof email === 'string';

    case 'user:deleted':
      return typeof email === 'undefined';

    default:
      return false;
  }
}

function handleEvent(input: unknown) {
  if (!isUserEvent(input)) {
    throw new Error('Evento inválido');
  }

  switch (input.type) {
    case 'user:created':
      console.log(`Criado: ${input.userId} (${input.email})`);
      break;

    case 'user:updatedEmail':
      console.log(`Email atualizado: ${input.userId} (${input.email})`);
      break;

    case 'user:deleted':
      console.log(`Deletado: ${input.userId}`);
      break;
  }
}

O ganho aqui

Você evita any e evita as Tipo. O compilador entende exatamente quais campos existem
em cada caso — e você só processa eventos quando eles realmente batem com o contrato.

Quer mais conteúdo prático?

Se você curte TypeScript bem feito, continua por aqui. No yurideveloper.com.br eu organizo posts técnicos
que ajudam você a melhorar código, reduzir bugs e deixar o compilador trabalhar a seu favor.

Feito para você aplicar no dia a dia: contratos explícitos, narrowing correto e validação na entrada.