TypeScript
Qualidade de Software
Arquitetura
Boas Práticas
Liderança Técnica

TypeScript Avançado na Prática: o Que Separa Uso Raso de Uso Maduro

TypeScript maduro não é sobre escrever mais tipos, é sobre deixar o compilador trabalhar a seu favor.

Adotar TypeScript é fácil. Usar bem é raro. A diferença entre as duas coisas não aparece no primeiro dia, ela aparece seis meses depois, quando o time precisa refatorar e descobre se os tipos são aliados ou enfeite.

Existe um TypeScript de superfície, em que tudo recebe uma anotação manual, any aparece sempre que a coisa fica difícil, e o compilador serve só para autocompletar nomes. E existe um TypeScript maduro, em que os tipos modelam o problema de verdade e o compilador vira um revisor incansável que pega erro antes do código rodar.

Este texto é sobre o segundo. Não como tutorial, mas como argumento sobre o valor de cada recurso para a saúde do código e do time.

Inferência: escrever menos para garantir mais

O primeiro instinto de quem chega ao TypeScript é anotar tudo. Cada variável, cada retorno, cada parâmetro recebe um tipo escrito à mão. Isso parece zeloso e na prática é o contrário.

Anotação manual em excesso cria ruído e, pior, cria mentira. O tipo escrito à mão pode divergir do valor real, e aí você tem uma anotação que documenta uma intenção que o código não cumpre mais. A inferência não mente: ela deriva o tipo do valor de fato.

O uso maduro confia na inferência onde ela é confiável e reserva a anotação para onde ela importa, que são as fronteiras. Assinatura de função pública, contrato de módulo, formato de dado que entra pela borda do sistema. No miolo, deixe o compilador inferir. Menos código, menos divergência, mais verdade.

Generics: a diferença entre reutilizar tipo e perder tipo

Generics costumam assustar porque a sintaxe parece acadêmica. O conceito é simples e profundamente prático: é como você escreve algo reutilizável sem jogar fora a informação de tipo no caminho.

Sem generics, a saída mais fácil para uma função que trabalha com qualquer coisa é tipar a entrada e a saída como genéricas demais, e o efeito é que o tipo se perde. Quem chama a função recebe de volta um valor sem forma e precisa adivinhar o que fazer com ele.

Com generics, a relação entre entrada e saída fica preservada. Uma função que recebe uma lista de algo devolve esse mesmo algo, e o compilador sabe disso na hora da chamada. Esse é o coração da reutilização segura: abstrair o comportamento sem abstrair a informação. Times que dominam generics escrevem menos utilitários duplicados e ganham autocomplete correto em todos eles.

Tipos utilitários: reaproveitar formato sem repetir formato

Todo sistema acumula tipos que são variações de outros tipos. A versão parcial de um objeto para um formulário, a versão sem o campo de senha para enviar ao cliente, a versão só de leitura de uma configuração.

A abordagem ingênua é redeclarar cada variação à mão. O problema aparece quando o tipo original muda: agora existem cinco cópias para atualizar e nenhuma garantia de que você lembrou de todas. É a mesma armadilha do código duplicado, só que em forma de tipo.

Tipos utilitários resolvem isso derivando uma forma a partir da outra. Quando o tipo base muda, as variações acompanham sozinhas, porque foram definidas em função dele. Isso transforma os tipos em um único ponto de verdade, e elimina aquela classe sutil de bug em que o código e o tipo dele foram atualizados em ritmos diferentes.

Narrowing: ensinar o compilador a raciocinar com você

Talvez o recurso mais subestimado seja o narrowing, que é a capacidade do TypeScript de estreitar um tipo conforme o fluxo do código avança. Você checa se um valor existe, e dentro daquele bloco o compilador passa a tratá-lo como existente.

Isso muda a forma como o time lida com o caso de borda. Em vez de espalhar verificações defensivas por todo lado e torcer, você modela os estados possíveis e deixa o compilador exigir que cada um seja tratado. O valor pode ser uma coisa ou outra, e o código só compila quando os dois caminhos foram cobertos.

O efeito cultural disso é grande. O nulo deixa de ser uma surpresa de produção e vira uma exigência de tempo de escrita. O time para de descobrir que esqueceu de um caso quando o cliente reclama, e passa a descobrir quando o editor reclama. É a diferença entre um bug e um lembrete.

A guerra contra o any

any é a válvula de escape do TypeScript, e como toda válvula de escape, ela é necessária em doses mínimas e destrutiva em doses normais. Um any não desliga o tipo daquela variável apenas. Ele contamina tudo que toca, porque qualquer coisa derivada de um any também vira any.

Uma base com any espalhado tem o pior dos dois mundos: paga o custo de configurar e manter TypeScript, mas perde as garantias justamente nos pontos mais arriscados, que são onde alguém não soube tipar e desistiu.

Times maduros tratam any como sinal de alerta, não como solução. Quando o tipo é desconhecido de verdade, existe a opção segura de marcá-lo como desconhecido e forçar uma verificação antes do uso, em vez de liberar tudo. A regra de bolso é direta: any deveria ser uma exceção rara, justificada e visível na revisão, nunca o caminho padrão para sair de um problema.

Modelar o domínio: o salto que mais paga

O nível mais alto de uso de TypeScript não é técnico, é de design. É usar os tipos para descrever as regras do negócio de tal forma que o estado inválido simplesmente não consiga existir.

Um pedido que pode estar pago ou pendente, mas nunca os dois. Um usuário que, quando é convidado, ainda não tem certos dados, e quando é ativo, obrigatoriamente tem. Quando você modela esses estados como tipos distintos, o compilador impede combinações impossíveis antes de qualquer teste rodar.

Isso muda a discussão do time. Em vez de "será que esse campo pode vir vazio aqui", a resposta está no tipo, explícita e verificável. O domínio fica documentado no código que executa, não em um documento que ninguém atualiza. Para a borda do sistema, onde dados externos chegam sem garantia nenhuma, vale combinar isso com validação em tempo de execução, e o caminho natural é a validação de dados com Zod, que conecta o dado real ao tipo.

Some tudo e o retorno não é estético, é operacional. Menos bug que chega em produção, porque o compilador pegou antes. Refatoração que o time encara sem medo, porque quebrar algo aparece na hora. Documentação que não envelhece, porque ela é o próprio código.

TypeScript maduro não é sobre escrever mais tipos. É sobre escrever os tipos certos nos lugares certos e deixar o compilador fazer o trabalho chato de verificar consistência, que é justamente o trabalho que humanos fazem mal e máquinas fazem bem.

Se o seu time já usa TypeScript mas ainda trata tipos como burocracia, o próximo passo é elevar a régua na revisão de código e tratar qualidade de tipo como parte da qualidade do código. Para ver como esses conceitos se conectam em uma arquitetura maior, vale seguir para monorepos com TypeScript.

Leia também

TypeScript Avançado na Prática: o Que Separa Uso Raso de Uso Maduro | Matheus Breguêz