Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis Já leu

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 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 transforma todas as propriedades de um tipo em opcionais. Isso é útil quando você precisa de

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.

Referências


Artigos relacionados