SkillsTecnológicas
Menu
Back-end

Tipagem em TypeScript: Guia Prático do Básico ao Avançado

Aprenda tipagem em TypeScript com exemplos de inferência, unions, narrowing, generics, strict, utility types e validação de dados externos.

Marcos RodriguesPublicado em 5 de outubro de 2024Atualizado em 18 de agosto de 202616 min de leitura
Desenvolvedor organizando módulos geométricos que representam tipos de dados em uma arquitetura de software

A tipagem em TypeScript ajuda a descrever quais valores um programa espera, como objetos se relacionam e quais estados são permitidos.

O compilador usa essas informações antes da execução para encontrar incompatibilidades, melhorar o autocomplete e tornar alterações mais previsíveis.

Isso não significa preencher cada linha com anotações nem transformar JavaScript em outra linguagem.

TypeScript entende boa parte do código por inferência, permite adotar tipos gradualmente e continua gerando JavaScript.

O ganho aparece quando os tipos representam regras reais do domínio, em vez de apenas repetir o que o código já diz.

Neste guia, você aprenderá desde tipos primitivos e funções até unions, narrowing, generics, utilitários e configuração estrita.

Também verá uma limitação essencial: os tipos são removidos na compilação, portanto respostas de API, formulários e arquivos ainda precisam ser validados durante a execução.

O que é tipagem em TypeScript?

Tipo é uma descrição dos valores e operações que o programa considera válidos. Se uma função recebe um identificador textual, o compilador pode impedir que um objeto seja enviado por engano.

Se um pedido possui estados definidos, uma union de literais pode impedir um estado inexistente.

function buscarUsuario(id: string) {
  return fetch(`/usuarios/${id}`);
}

buscarUsuario("usr_42"); // válido
buscarUsuario(42);       // erro antes da execução

TypeScript realiza essa análise durante o desenvolvimento e a compilação. Depois, as anotações são removidas e o ambiente executa JavaScript.

Essa separação explica por que TypeScript melhora a segurança de mudanças, mas não verifica sozinho um JSON vindo da internet.

A documentação de tipos cotidianos do TypeScript começa por string, number, boolean, arrays, objetos, unions e inferência. Essas peças simples formam a base dos recursos avançados.

Tipagem estática, estrutural e gradual

A tipagem é estática porque o código é verificado antes da execução. O termo não significa que valores nunca mudam; significa que o compilador acompanha quais mudanças são compatíveis com as declarações.

Ela é estrutural porque a compatibilidade depende principalmente da forma do valor. Um objeto pode atender a uma interface sem declarar que a implementa, desde que tenha as propriedades exigidas.

interface Identificavel {
  id: string;
}

const produto = {
  id: "prod_10",
  nome: "Teclado"
};

function imprimirId(item: Identificavel) {
  console.log(item.id);
}

imprimirId(produto); // compatível pela estrutura

Também é gradual: um projeto pode combinar arquivos JavaScript e TypeScript e endurecer as verificações por etapas. Recursos como any permitem escapar do sistema, embora seu uso excessivo elimine justamente as garantias procuradas.

Por isso, chamar TypeScript apenas de “linguagem fortemente tipada” simplifica demais. Uma descrição mais útil é: verificador estático estrutural para JavaScript, com inferência e adoção gradual. Para comparar as duas experiências, veja TypeScript vs JavaScript.

Inferência ou anotação explícita?

TypeScript infere o tipo a partir do valor e do contexto. Anotar o que já está evidente aumenta ruído sem acrescentar contrato.

const tentativas = 3;           // number
const ativo = true;             // boolean
const nomes = ["Ana", "Bia"];   // string[]

Anotações são valiosas nas fronteiras: parâmetros de funções públicas, retorno de APIs internas, propriedades de componentes, formatos persistidos e objetos compartilhados entre módulos. Elas expressam intenção e impedem que uma implementação altere silenciosamente um contrato.

type Resultado = {
  total: number;
  itens: string[];
};

function pesquisar(termo: string): Resultado {
  return {
    total: 1,
    itens: [termo]
  };
}

Uma regra prática é deixar o compilador inferir variáveis locais e declarar contratos onde dados atravessam uma fronteira. Se a anotação obriga repetir um tipo grande, extraia um alias ou derive o tipo de uma fonte existente.

Tipos primitivos, arrays, tuplas e objetos

Use os nomes minúsculos string, number e boolean. String, Number e Boolean representam wrappers que raramente pertencem a contratos de aplicação.

const titulo: string = "Relatório";
const quantidade: number = 12;
const publicado: boolean = false;

const ids: string[] = ["a", "b"];
const valores: Array<number> = [10, 20];

const coordenada: [number, number] = [-23.5, -46.6];

Array contém uma coleção variável de elementos do mesmo tipo. Tupla descreve posições conhecidas e possivelmente diferentes.

Não use tupla longa para substituir um objeto: nomes de propriedades tornam a intenção mais clara.

type Usuario = {
  id: string;
  nome: string;
  email?: string;
};

const usuario: Usuario = {
  id: "usr_1",
  nome: "Marina"
};

O sinal ? diz que a propriedade pode estar ausente. Ele não deve ser usado apenas para silenciar erro de inicialização. Se o dado é necessário para a regra funcionar, mantenha-o obrigatório e ajuste a origem.

Unions, tipos literais e narrowing

Uma union representa alternativas. Tipos literais restringem valores às opções declaradas. Juntos, eles modelam estados com precisão.

type StatusPedido =
  | "recebido"
  | "em_preparo"
  | "enviado"
  | "entregue";

function atualizarStatus(status: StatusPedido) {
  // ...
}

Para usar uma operação exclusiva de um membro da union, o código precisa estreitar o tipo. Esse processo é chamado de narrowing e acompanha verificações reais como typeof, instanceof, igualdade, presença de propriedade e funções de guarda.

Objetos de dados de formatos variados sendo separados por etapas de verificação
O narrowing usa verificações do fluxo de controle para reduzir uma união ao tipo seguro em cada ramo do código.
function formatar(valor: string | number) {
  if (typeof valor === "string") {
    return valor.trim();
  }

  return valor.toFixed(2);
}

Unions discriminadas usam uma propriedade literal comum para diferenciar objetos:

type Pagamento =
  | { tipo: "pix"; chave: string }
  | { tipo: "cartao"; ultimosDigitos: string };

function resumir(pagamento: Pagamento) {
  if (pagamento.tipo === "pix") {
    return `PIX: ${pagamento.chave}`;
  }

  return `Cartão: ${pagamento.ultimosDigitos}`;
}

A página oficial sobre narrowing no TypeScript detalha como a análise de fluxo reduz tipos em cada ramo. Esse recurso é mais seguro que adicionar propriedades opcionais a um único objeto para representar todos os estados.

Type ou interface: qual usar?

interface e type conseguem descrever formas de objetos. Para muitos casos, a escolha é de consistência.

Interfaces suportam extensão e declaração combinada; aliases também nomeiam unions, tuplas, primitivas e composições.

interface Pessoa {
  nome: string;
}

interface Cliente extends Pessoa {
  limite: number;
}

type Identificador = string | number;

type Produto = {
  id: Identificador;
  nome: string;
};

Use interface quando um contrato de objeto aberto e extensível combina com o domínio ou com a biblioteca.

Use type quando precisar de union, tupla, tipo condicional ou composição. Evite uma regra dogmática e mantenha o padrão do projeto.

Interseções como A & B exigem atender às duas estruturas. Elas são úteis, mas uma composição impossível pode produzir propriedades do tipo never.

Para entidades de domínio, uma interface ou objeto explícito costuma gerar mensagens mais fáceis de compreender.

Como tipar funções corretamente

Parâmetros formam a entrada do contrato; o retorno descreve o resultado. Em funções locais simples, o retorno pode ser inferido.

Em APIs exportadas ou regras importantes, declará-lo evita que uma mudança interna altere consumidores sem aviso.

type CriarPedido = {
  clienteId: string;
  itens: Array<{
    produtoId: string;
    quantidade: number;
  }>;
};

type PedidoCriado = {
  id: string;
  status: "recebido";
};

async function criarPedido(
  entrada: CriarPedido
): Promise<PedidoCriado> {
  // persistência omitida
  return {
    id: "ped_100",
    status: "recebido"
  };
}

Callbacks também carregam tipos. O contexto frequentemente infere o parâmetro:

const precos = [20, 35, 40];
const comDesconto = precos.map(preco => preco * 0.9);

Não torne parâmetros opcionais quando a função não sabe operar sem eles. E não declare retorno como Promise<any>: isso espalha ausência de verificação por toda a cadeia assíncrona.

Generics sem perder informação

Generics expressam relações entre tipos. Ao contrário de any, eles preservam informação de entrada e saída.

function primeiro<T>(itens: T[]): T | undefined {
  return itens[0];
}

const nome = primeiro(["Ana", "Caio"]);
// string | undefined

const numero = primeiro([10, 20]);
// number | undefined

Uma restrição limita quais capacidades o tipo genérico precisa oferecer:

function obterPropriedade<
  T,
  K extends keyof T
>(objeto: T, chave: K): T[K] {
  return objeto[chave];
}

const livro = {
  titulo: "Arquitetura",
  paginas: 320
};

obterPropriedade(livro, "titulo"); // string
obterPropriedade(livro, "preco");  // erro

A documentação oficial de generics enfatiza componentes reutilizáveis que mantêm relações precisas. Evite criar parâmetros genéricos usados apenas uma vez e sem conectar valores; um tipo concreto costuma ser mais legível.

any, unknown, never e void

any desativa a verificação para o valor e contamina operações seguintes. É útil como ponte temporária em migrações, mas deve ser localizado e acompanhado de tarefa para remoção.

unknown aceita qualquer valor, porém exige verificação antes do uso. Por isso, é uma entrada melhor para JSON, erros capturados e dados externos:

function obterMensagem(erro: unknown): string {
  if (erro instanceof Error) {
    return erro.message;
  }

  return "Erro desconhecido";
}

void descreve que o retorno de uma função não será usado como valor. never representa algo que não acontece, como uma função que sempre lança ou um ramo impossível. Ele ajuda em verificações exaustivas:

function assertNunca(valor: never): never {
  throw new Error(`Estado não tratado: ${valor}`);
}

type Papel = "admin" | "editor";

function permissao(papel: Papel) {
  switch (papel) {
    case "admin":
      return "total";
    case "editor":
      return "conteudo";
    default:
      return assertNunca(papel);
  }
}

Null, propriedades opcionais e readonly

Com strictNullChecks, ausência faz parte do tipo. Se uma busca pode falhar, o retorno deve comunicar undefined ou outra forma explícita de resultado.

function encontrarUsuario(
  id: string
): Usuario | undefined {
  return usuarios.find(usuario => usuario.id === id);
}

const encontrado = encontrarUsuario("usr_1");

if (encontrado) {
  console.log(encontrado.nome);
}

Optional chaining, ?., interrompe o acesso quando encontra null ou undefined. O operador ?? fornece fallback somente para esses dois valores, preservando 0, string vazia e false.

const cidade = usuario.endereco?.cidade ?? "Não informada";

readonly impede reatribuição pelo sistema de tipos, mas não congela o objeto em runtime. Para estruturas profundamente imutáveis, modele os níveis necessários e considere as garantias reais da biblioteca ou da plataforma.

Utility types e tipos derivados

Duplicar manualmente o mesmo objeto para criação, edição, listagem e resposta facilita divergências. Utility types derivam novas formas de uma definição existente.

type Perfil = {
  id: string;
  nome: string;
  email: string;
  criadoEm: Date;
};

type CriarPerfil = Omit<Perfil, "id" | "criadoEm">;
type AtualizarPerfil = Partial<CriarPerfil>;
type PerfilPublico = Pick<Perfil, "id" | "nome">;
type PerfisPorId = Record<string, Perfil>;

Partial torna propriedades opcionais; Required faz o oposto; Pick seleciona chaves; Omit remove; Record descreve um mapa. A referência de utility types do TypeScript reúne as transformações globais disponíveis.

Tipos mapeados, condicionais e template literal types permitem transformações mais específicas. Use-os quando reduzem repetição e tornam a API mais precisa.

Se uma definição avançada exige mais tempo para decifrar que o código protegido, simplifique ou esconda a complexidade atrás de um nome bem documentado.

O artigo sobre tipagem avançada em TypeScript aprofunda essas ferramentas.

Assertions e satisfies: use sem esconder erros

Uma assertion com as informa ao compilador que você conhece mais sobre o valor.

Ela não converte nem valida dados. Usá-la para silenciar uma incompatibilidade apenas desloca o erro para a execução.

const entrada: unknown = JSON.parse(texto);

// Perigoso: nenhuma validação aconteceu
const usuario = entrada as Usuario;

O operador satisfies verifica se uma expressão atende a um tipo sem substituir desnecessariamente o tipo específico inferido:

type Ambiente = "dev" | "prod";

const urls = {
  dev: "http://localhost:3000",
  prod: "https://api.exemplo.com"
} satisfies Record<Ambiente, string>;

Use annotation quando o valor deve assumir aquele contrato, satisfies quando quer verificar compatibilidade preservando inferência e assertion somente quando existe informação real que o compilador não consegue obter.

O operador de não nulo, !, também é uma assertion: aplique apenas quando uma invariante foi garantida fora do alcance da análise.

Tipos não validam dados em runtime

Uma resposta de API pode estar incompleta, um formulário pode ser manipulado e um arquivo pode não seguir o formato esperado. Declarar o resultado de response.json() como Usuario não altera os bytes recebidos.

Dados externos atravessando uma barreira de validação antes de entrar em uma aplicação estruturada
Tipos ajudam durante o desenvolvimento, enquanto a validação em runtime protege a aplicação de dados externos inesperados.

Trate entradas externas como unknown e valide antes de levá-las ao núcleo tipado.

Você pode escrever um guarda para formas simples ou usar uma biblioteca de schema quando precisa de mensagens, transformação e composição.

type UsuarioApi = {
  id: string;
  nome: string;
};

function ehUsuarioApi(
  valor: unknown
): valor is UsuarioApi {
  if (typeof valor !== "object" || valor === null) {
    return false;
  }

  const objeto = valor as Record<string, unknown>;

  return (
    typeof objeto.id === "string" &&
    typeof objeto.nome === "string"
  );
}

O cast interno é limitado ao código que já verificou objeto não nulo e serve apenas para acessar chaves desconhecidas. A função só retorna verdadeiro depois de conferir propriedades.

Em payloads aninhados, arrays, datas ou regras de negócio, um schema testado costuma ser mais sustentável.

Essa separação é fundamental em front-end e back-end. A aplicação valida a fronteira uma vez e trabalha internamente com dados confiáveis.

Para entender contratos, autenticação, erros e versionamento além dos tipos, consulte o guia de desenvolvimento de APIs.

Configuração strict para projetos reais

A opção strict habilita uma família de verificações que produz garantias mais fortes.

A documentação de strict no TSConfig observa que versões futuras podem acrescentar checagens à família; por isso, atualizações do compilador podem revelar novos erros legítimos.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "useUnknownInCatchVariables": true,
    "noEmitOnError": true
  }
}

noUncheckedIndexedAccess acrescenta undefined a acessos por índice que não podem ser provados. exactOptionalPropertyTypes diferencia propriedade ausente de uma propriedade presente com valor undefined. noImplicitOverride pede intenção explícita ao sobrescrever membros de classe.

Nem toda base antiga consegue ativar tudo de uma vez. Comece por strict ou por opções de maior impacto, corrija por módulos e mantenha a checagem no CI. Não desligue uma regra global para contornar um único pacote; isole o ponto e documente a exceção.

Exemplo prático: modelando uma resposta de API

Considere uma tela que lista pedidos. O domínio precisa distinguir sucesso e falha sem depender de propriedades opcionais soltas.

type Pedido = {
  id: string;
  total: number;
  status: "aberto" | "pago" | "cancelado";
};

type Resultado<T> =
  | { ok: true; dados: T }
  | { ok: false; erro: string };

async function listarPedidos(): Promise<
  Resultado<Pedido[]>
> {
  try {
    const resposta = await fetch("/api/pedidos");

    if (!resposta.ok) {
      return {
        ok: false,
        erro: `HTTP ${resposta.status}`
      };
    }

    const dados: unknown = await resposta.json();

    if (!ehListaDePedidos(dados)) {
      return {
        ok: false,
        erro: "Resposta inválida"
      };
    }

    return { ok: true, dados };
  } catch (erro: unknown) {
    return {
      ok: false,
      erro: obterMensagem(erro)
    };
  }
}

O consumidor usa o discriminante ok:

const resultado = await listarPedidos();

if (resultado.ok) {
  resultado.dados.forEach(pedido => {
    console.log(pedido.total);
  });
} else {
  console.error(resultado.erro);
}

O exemplo conecta generics, union discriminada, narrowing, unknown e validação de runtime. A tipagem descreve o fluxo completo, sem afirmar que a rede entregará dados corretos.

Erros comuns ao usar TypeScript

  • Usar any em toda integração: a ausência de verificação se espalha para consumidores.
  • Fazer cast logo após JSON: assertion não valida formato nem converte valores.
  • Anotar tudo: repetição reduz legibilidade e pode ampliar tipos que seriam inferidos com precisão.
  • Adicionar opcionais para eliminar erros: o domínio passa a aceitar estados incompletos.
  • Usar non-null assertion por hábito: o compilador para de lembrar um caso que pode ocorrer.
  • Criar generics sem relação: complexidade cresce sem preservar informação útil.
  • Modelar todos os estados em uma interface: unions discriminadas costumam impedir combinações inválidas.
  • Confundir readonly com congelamento: a garantia é estática e pode não ser profunda.
  • Ignorar erros na compilação de produção: emitir mesmo com falhas torna a checagem opcional.

Outro erro é estudar recursos avançados antes de dominar JavaScript. TypeScript analisa conceitos como objetos, closures, promises, módulos e fluxo de controle; ele não os substitui.

Se essa base ainda estiver em formação, comece pelo guia de JavaScript moderno.

Como adotar tipagem em um projeto existente

Uma migração segura deve manter o produto funcionando. Configure o compilador, permita JavaScript temporariamente e escolha módulos com fronteiras claras.

Tipar primeiro funções compartilhadas, modelos e integrações produz mais valor que converter arquivos por tamanho.

  1. Crie o TSConfig e faça a verificação rodar localmente e no CI.
  2. Mapeie entradas externas, contratos públicos e pontos com mais defeitos.
  3. Converta um fluxo vertical pequeno, incluindo testes.
  4. Troque any por tipos concretos ou unknown com narrowing.
  5. Ative verificações estritas por etapas e registre exceções.
  6. Padronize convenções de types, interfaces, retornos e validação.
  7. Atualize dependências e resolva incompatibilidades sem casts globais.
  8. Acompanhe a redução de erros, não apenas a porcentagem de arquivos convertidos.

Em aplicações de servidor, tipagem ajuda a manter contratos entre rotas, serviços e persistência, mas precisa caminhar com testes e observabilidade.

O guia de Node.js para back-end mostra onde TypeScript se encaixa nessa arquitetura.

Quem está começando do zero pode seguir o guia de TypeScript para iniciantes.

Este artigo deve servir como referência específica do sistema de tipos, enquanto o guia geral cobre instalação, ferramentas e primeiros projetos.

Perguntas frequentes

TypeScript verifica tipos durante a execução?

Não. As anotações são removidas na compilação. Para validar API, formulário, variável de ambiente ou arquivo, use verificações em JavaScript ou uma biblioteca de schema.

Devo evitar any completamente?

O ideal é evitar em código novo e contratos. Em migrações ou definições incompletas, pode existir temporariamente. Mantenha o uso pequeno, documentado e próximo da fronteira. Quando o valor é desconhecido, prefira unknown.

Type é melhor que interface?

Nenhum é universalmente melhor. Ambos descrevem objetos. Interface combina bem com contratos extensíveis; type é necessário para unions, tuplas e várias transformações. Consistência e clareza importam mais que preferência absoluta.

Vale declarar o retorno de todas as funções?

Não é obrigatório. A inferência funciona bem em funções locais. Declare retornos de APIs exportadas, regras centrais e funções nas quais uma alteração acidental do contrato seria cara.

Generics tornam o código mais seguro?

Quando expressam uma relação real entre entrada, saída ou propriedades. Um generic desnecessário pode tornar mensagens e leitura piores. Comece pelo tipo mais simples que preserva a informação necessária.

Ativar strict quebra o projeto?

Ele pode revelar muitos problemas em uma base permissiva. Isso não muda o runtime por si só. Faça a adoção em etapas, bloqueie novos problemas e corrija módulos com testes, sem mascarar tudo com casts.

Conclusão

Boa tipagem em TypeScript não é a que usa mais sintaxe, e sim a que torna estados inválidos difíceis de representar.

Inferência reduz ruído; unions e narrowing descrevem alternativas; generics preservam relações; utilitários evitam duplicação; strict amplia garantias.

Ao mesmo tempo, mantenha a fronteira entre análise estática e realidade: valide dados externos, teste comportamento e trate erros de rede.

Com esses hábitos, os tipos deixam de ser decoração e passam a funcionar como contratos vivos que acompanham a evolução do software.