TypeScript: generics e utility types na prática
Entenda pra que servem generics e os utility types mais usados do TypeScript (Partial, Pick, Omit, Record), com exemplo prático de cada um, sem precisar decorar sintaxe de teoria de tipos.
Já mencionei TypeScript em vários posts aqui, desde a polêmica em volta dele até validação com Zod, mas sempre passando batido por um ponto que trava bastante gente na transição do JavaScript puro: generics e utility types. A sintaxe parece assustadora à primeira vista, cheia de <T> espalhado, mas o conceito por trás é bem mais simples do que parece.
O problema que generics resolve
Imagina uma função que recebe qualquer valor e devolve ele dentro de um array:
function embrulhar(valor: any) {
return [valor];
}Funciona, mas any desliga a checagem de tipo. Se você chamar embrulhar(10), o TypeScript não sabe que o retorno é number[], ele só sabe que é "qualquer coisa". Você perdeu a proteção de tipo justamente no lugar onde ela mais ajudaria.
Um generic resolve isso criando uma espécie de variável de tipo, que se ajusta de acordo com o que você passa:
function embrulhar<T>(valor: T): T[] {
return [valor];
}
const numeros = embrulhar(10); // number[]
const nomes = embrulhar("Ana"); // string[]O <T> aqui não é um tipo fixo, é um espaço reservado. Quando você chama embrulhar(10), o TypeScript substitui T por number automaticamente, e o retorno vira number[]. Quando você chama com uma string, T vira string. É a mesma função, mas o tipo se adapta a cada chamada, sem precisar escrever uma versão pra cada tipo possível nem abrir mão da checagem com any.
Onde isso aparece no dia a dia
Você provavelmente já usou generics sem perceber. Array<string>, Promise<Response>, Map<string, number> são todos generics: o Array em si não sabe o que vai guardar, é o <string> que diz isso pra ele. O mesmo vale pra uma função que busca dado de uma API:
async function buscar<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json();
}
const usuario = await buscar<{ nome: string; idade: number }>("/api/usuario");
console.log(usuario.nome); // TypeScript sabe que isso existeSem o generic, buscar teria que devolver any, e você perderia autocomplete e checagem em todo lugar que chamasse essa função.
Utility types: transformando tipos que já existem
Além de generics, o TypeScript vem com um conjunto de utilitários prontos que pegam um tipo existente e geram uma variação dele, sem você precisar reescrever tudo na mão. Os quatro mais usados no dia a dia:
type Usuario = {
id: string;
nome: string;
email: string;
senha: string;
};Partial<T> deixa todos os campos opcionais. Útil em função de atualização, onde a pessoa pode mandar só os campos que quer mudar:
function atualizarUsuario(id: string, dados: Partial<Usuario>) {
// dados.nome, dados.email etc, todos opcionais
}Pick<T, K> cria um novo tipo com só alguns campos escolhidos do original:
type UsuarioPublico = Pick<Usuario, "id" | "nome">;
// { id: string; nome: string }Omit<T, K> faz o oposto, pega tudo menos os campos listados. É o mais comum pra esconder dado sensível num retorno de API:
type UsuarioSemSenha = Omit<Usuario, "senha">;
// tudo de Usuario, exceto senhaRecord<K, V> cria um objeto com chaves de um tipo e valores de outro, útil pra mapear algo por categoria:
type ContagemPorStatus = Record<"ativo" | "pendente" | "cancelado", number>;
// { ativo: number; pendente: number; cancelado: number }Por que usar isso em vez de reescrever o tipo
O ponto central de todos esses utilities é o mesmo: evitar duplicar a definição de um tipo em vários lugares. Se Usuario ganhar um campo novo amanhã, Omit<Usuario, "senha"> já reflete isso automaticamente, sem você precisar lembrar de atualizar um segundo tipo escrito à mão em outro arquivo. Menos lugares pra manter sincronizados, menos chance de um tipo ficar desatualizado sem ninguém perceber.
Fechando
Generics resolvem o problema de escrever código reutilizável sem abrir mão de tipo, e utility types resolvem o problema de derivar variações de um tipo que já existe, sem repetir a definição inteira de novo. Nenhum dos dois é sobre decorar sintaxe, é sobre reconhecer o problema (função genérica demais, ou tipo quase igual a outro que já existe) e saber que o TypeScript já tem uma ferramenta pronta pra isso, sem você precisar reinventar.
Leia também