Introdução aos Utility Types
Os Utility Types são uma funcionalidade poderosa do TypeScript que permite transformar tipos existentes em novos tipos de forma declarativa e reutilizável. Diferentemente de criar tipos do zero, você trabalha com tipos já existentes e aplica operações sobre eles — como uma "função de tipo". Isso reduz duplicação de código, aumenta a manutenibilidade e deixa seu sistema de tipos mais robusto.
A principal vantagem é que quando você altera um tipo base, todas as transformações realizadas através de Utility Types são atualizadas automaticamente. Imagine ter uma interface User com 20 propriedades; você pode derivar dela um tipo que contém apenas alguns campos, outro que torna tudo opcional, outro que apenas leitura — tudo sem duplicar código. Nesta aula, vamos explorar os Utility Types mais essenciais e suas aplicações práticas.
Partial, Required e Readonly: Modificando Obrigatoriedade e Mutabilidade
Partial: Tornando Propriedades Opcionais
Partial<T> transforma todas as propriedades de um tipo em opcionais. Isso é útil quando você precisa de um tipo que permite atualizações parciais ou valores padrão incompletos.
interface Usuario {
id: number;
nome: string;
email: string;
telefone: string;
}
// Sem Partial, você precisaria recriar a interface inteira com ? em cada propriedade
type UsuarioAtualizado = Partial<Usuario>;
// Agora todas as propriedades são opcionais
const atualizacao: UsuarioAtualizado = {
nome: "João Silva"
// email, telefone e id não são obrigatórios
};
function atualizarUsuario(id: number, dados: UsuarioAtualizado): void {
// Implementação que atualiza apenas os campos fornecidos
console.log(`Atualizando usuário ${id} com:`, dados);
}
A razão por trás disso é simples: em APIs REST, por exemplo, você raramente recebe todos os campos de uma entidade para atualização. Partial reduz o boilerplate e deixa claro a intenção.
Required: Tornando Propriedades Obrigatórias
Required<T> faz o oposto — transforma propriedades opcionais em obrigatórias. Útil quando você tem um tipo com muitos campos opcionais e precisa garantir que em um contexto específico todos sejam fornecidos.
interface Configuracao {
tema?: "claro" | "escuro";
idioma?: string;
notificacoes?: boolean;
fontSize?: number;
}
// Todas as propriedades agora são obrigatórias
type ConfiguracaoCompleta = Required<Configuracao>;
const config: ConfiguracaoCompleta = {
tema: "claro",
idioma: "pt-BR",
notificacoes: true,
fontSize: 14
// Remova qualquer uma dessas propriedades e terá erro de compilação
};
Readonly: Tornando Propriedades Imutáveis
Readonly<T> marca todas as propriedades como apenas leitura. Isso previne modificações acidentais e é especialmente valioso em código imutável ou para dados sensíveis.
interface Produto {
id: number;
nome: string;
preco: number;
}
type ProdutoImuvel = Readonly<Produto>;
const produto: ProdutoImuvel = {
id: 1,
nome: "Notebook",
preco: 3000
};
// Erro de compilação: Cannot assign to 'preco' because it is a read-only property
// produto.preco = 2500;
Pick, Omit e Record: Selecionando e Criando Tipos Estruturados
Pick: Selecionando Propriedades Específicas
Pick<T, K> permite selecionar apenas as propriedades que você deseja de um tipo. Diferente de Partial, ele não muda obrigatoriedade — apenas filtra quais campos existem.
interface Pessoa {
id: number;
nome: string;
email: string;
endereco: string;
telefone: string;
}
// Pegamos apenas id e nome
type PessoaResumo = Pick<Pessoa, "id" | "nome">;
const resumo: PessoaResumo = {
id: 1,
nome: "Maria"
// email, endereco e telefone não existem aqui
};
// Também funciona com tipos de função
type DadosContato = Pick<Pessoa, "email" | "telefone">;
function enviarMensagem(dados: DadosContato): void {
console.log(`Enviando para ${dados.email}`);
}
Pick é fundamental quando você quer subconjuntos de tipos para DTOs (Data Transfer Objects), respostas de API ou argumentos de função específicos.
Omit: Excluindo Propriedades
Omit<T, K> é o inverso do Pick — você define quais propriedades remover, não quais manter.
interface Artigo {
id: number;
titulo: string;
conteudo: string;
autorId: number;
criadoEm: Date;
atualizadoEm: Date;
}
// Removemos os campos de auditoria (data/hora)
type ArtculoSemAuditoria = Omit<Artigo, "criadoEm" | "atualizadoEm">;
const artigo: ArtculoSemAuditoria = {
id: 1,
titulo: "TypeScript na Prática",
conteudo: "...",
autorId: 5
};
// Use quando você quer "quase tudo" de um tipo, exceto alguns campos
type ArtgigoPublico = Omit<Artigo, "autorId" | "criadoEm" | "atualizadoEm">;
Use Omit quando deseja trabalhar com "a maioria" das propriedades. Se precisar de apenas poucos campos, Pick é mais semântico.
Record: Criando Objetos com Chaves Tipadas
Record<K, T> cria um tipo de objeto onde as chaves são de um tipo K e os valores são de tipo T. Útil para mapas tipados e enums.
type Perfil = "admin" | "usuario" | "visitante";
// Garante que temos uma entrada para cada perfil
type PermissoesPerPerfil = Record<Perfil, string[]>;
const permissoes: PermissoesPerPerfil = {
admin: ["ler", "escrever", "deletar", "gerenciar"],
usuario: ["ler", "escrever"],
visitante: ["ler"]
// Se você remover qualquer uma dessas chaves, terá erro
};
// Também funciona com union types numéricos
type StatusCode = 200 | 404 | 500;
type MensagensErro = Record<StatusCode, string>;
const mensagens: MensagensErro = {
200: "OK",
404: "Não encontrado",
500: "Erro interno"
};
Record é particularmente poderoso em situações onde você precisa mapear enums ou unions para valores e quer garantir cobertura completa em tempo de compilação.
Exclude, Extract e Conditional Types: Manipulando Union Types
Exclude: Removendo Tipos de uma Union
Exclude<T, U> remove tipos de uma union que correspondem a U. Trabalha especificamente com tipos primitivos e literals, não com interfaces.
type Status = "pendente" | "aprovado" | "rejeitado" | "cancelado";
// Removemos o status "cancelado"
type StatusAtivo = Exclude<Status, "cancelado">;
const status: StatusAtivo = "pendente"; // OK
// const status2: StatusAtivo = "cancelado"; // Erro!
// Mais útil com tipos complexos
type Tipos = string | number | boolean | null | undefined;
type TiposValidos = Exclude<Tipos, null | undefined>;
const valor: TiposValidos = "texto"; // OK
const nulo: TiposValidos = null; // Erro!
Extract: Mantendo Apenas Tipos Específicos
Extract<T, U> faz o oposto — mantém apenas os tipos que correspondem a U, removendo todo o resto.
type Evento = "click" | "scroll" | "resize" | "load" | "error";
type EventosDeMouse = Extract<Evento, "click" | "scroll">;
const evento: EventosDeMouse = "click"; // OK
// const evento2: EventosDeMouse = "load"; // Erro!
// Use para filtrar tipos de uma grande union
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
type MethodosInseguros = Extract<HTTPMethod, "POST" | "PUT" | "DELETE" | "PATCH">;
Conditional Types: Lógica Dentro do Sistema de Tipos
Conditional types permitem criar tipos que dependem de outras condições. A sintaxe é T extends U ? X : Y.
// Exemplo: Se o tipo é array, extraia o elemento; caso contrário, devolva o tipo
type Flatten<T> = T extends Array<infer U> ? U : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
// Mais prático: Verificar se é uma função e extrair tipo de retorno
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function saudar(nome: string): string {
return `Olá, ${nome}`;
}
type ResultadoSaudacao = ReturnType<typeof saudar>; // string
Conditional types são avançados, mas permitem criar abstrações poderosas e reutilizáveis para seus tipos.
Exemplo Prático: Integrando Utility Types em Uma Aplicação Real
Vamos criar um exemplo completo que demonstra como esses Utility Types trabalham juntos em um cenário realista de API com autenticação.
// Interface base
interface Usuario {
id: number;
nome: string;
email: string;
senha: string;
telefone?: string;
role: "admin" | "usuario";
ativo: boolean;
criadoEm: Date;
}
// DTO para criação (sem id e datas, sem role)
type CriarUsuarioDTO = Omit<Usuario, "id" | "criadoEm" | "role">;
// Para atualizar, tudo é opcional
type AtualizarUsuarioDTO = Partial<Omit<Usuario, "id" | "criadoEm">>;
// Resposta pública (sem senha)
type UsuarioPublico = Omit<Usuario, "senha">;
// Apenas dados de contato
type ContatoUsuario = Pick<Usuario, "email" | "telefone">;
// Estados de autenticação
type AuthStatus = "autenticado" | "nao-autenticado" | "expirado";
type AuthStatusValido = Exclude<AuthStatus, "expirado">;
// Permissões por role
type RolePermissoes = Record<Usuario["role"], string[]>;
const permissoes: RolePermissoes = {
admin: ["ler", "escrever", "deletar", "gerenciar-usuarios"],
usuario: ["ler", "escrever"]
};
// Simulando uma API
class UsuarioService {
// Criação: precisa do DTO específico
criar(dados: CriarUsuarioDTO): UsuarioPublico {
console.log("Criando usuário com:", dados);
return {
id: 1,
...dados,
criadoEm: new Date(),
ativo: true
};
}
// Atualização: Partial permite campos opcionais
atualizar(id: number, dados: AtualizarUsuarioDTO): UsuarioPublico {
console.log(`Atualizando usuário ${id} com:`, dados);
return {} as UsuarioPublico;
}
// Retorna apenas dados públicos
obter(id: number): UsuarioPublico {
return {} as UsuarioPublico;
}
// Retorna apenas contato
obterContato(id: number): ContatoUsuario {
return {} as ContatoUsuario;
}
}
// Uso
const service = new UsuarioService();
// Erro: senha é obrigatória em CriarUsuarioDTO
// service.criar({ nome: "João", email: "joao@example.com" });
// OK: todos os campos obrigatórios presentes
service.criar({
nome: "João Silva",
email: "joao@example.com",
senha: "segura123",
ativo: true,
role: "usuario"
});
// OK: Partial permite qualquer combinação
service.atualizar(1, { nome: "João Silva Junior" });
Este exemplo mostra como os Utility Types trabalham de mãos dadas: você define uma interface base (Usuario) e cria derivações para casos específicos (DTOs, respostas públicas, subconjuntos). Isso elimina duplicação, mantém sincronização automática e deixa o contrato de dados explícito.
Outros Utility Types Importantes
NonNullable: Removendo null e undefined
NonNullable<T> remove null e undefined de uma union.
type ValorOuNada = string | null | undefined;
type ValorSeguro = NonNullable<ValorOuNada>; // string
const valor: ValorSeguro = "texto"; // OK
// const nulo: ValorSeguro = null; // Erro!
Readonly com Arrays
ReadonlyArray<T> ou readonly T[] torna um array imutável.
type ListaImutavel = readonly string[];
const cores: ListaImutavel = ["vermelho", "azul"];
// Erro: Property 'push' does not exist on type 'readonly string[]'
// cores.push("verde");
keyof: Obtendo Chaves de um Tipo
keyof T extrai as chaves de um tipo como uma union. Muito útil com genéricos.
interface Config {
timeout: number;
retries: number;
debug: boolean;
}
type ConfigKeys = keyof Config; // "timeout" | "retries" | "debug"
function obterConfig<K extends keyof Config>(chave: K): Config[K] {
return {} as Config[K];
}
const timeout = obterConfig("timeout"); // number
Conclusão
Os Utility Types são essenciais para escrita de TypeScript profissional e escalável. Três pontos principais a levar: primeiro, eles eliminam duplicação de código ao permitir transformações de tipos existentes — ao invés de recriar interfaces manualmente, você compõe tipos através de operações declarativas. Segundo, mantêm sincronização automática — quando a interface base muda, todos os tipos derivados são atualizados instantaneamente, evitando inconsistências. Terceiro, melhoram a semântica e intenção do código — Omit<Artigo, "senha"> deixa absolutamente claro que você quer "tudo menos senha", enquanto Pick<Usuario, "email" | "telefone"> mostra exatamente quais campos serão usados.
Domine Partial, Required, Pick, Omit e Record como base, depois explore Exclude, Extract, e conditional types conforme sua aplicação crescer. O investimento em entender esses mecanismos agora vai economizar horas de refatoração e bugs no futuro.