Tipos utilitários: Partial, Required, Pick, Omit, Record

1. Introdução aos Tipos Utilitários no TypeScript

Os tipos utilitários são ferramentas nativas do TypeScript que permitem transformar tipos existentes de maneira flexível e reutilizável. Introduzidos para aumentar a produtividade do desenvolvedor, eles reduzem a necessidade de criar tipos manualmente, aproveitando o sistema de tipos estáticos da linguagem. Em vez de redefinir variações de um mesmo tipo, você aplica transformações predefinidas.

Neste artigo, exploraremos cinco tipos utilitários essenciais: Partial, Required, Pick, Omit e Record. Cada um resolve problemas comuns no desenvolvimento, como manipulação de formulários, atualizações parciais, segurança de dados e mapeamentos estruturados.

2. Partial: Tornando Propriedades Opcionais

Partial<T> transforma todas as propriedades de T em opcionais. É ideal para cenários onde você precisa representar estados incompletos, como formulários parciais ou atualizações incrementais.

interface Config {
  url: string;
  timeout: number;
  retries: number;
}

// Tipo com todas as propriedades opcionais
type PartialConfig = Partial<Config>;

function updateConfig(original: Config, updates: Partial<Config>): Config {
  return { ...original, ...updates };
}

const defaultConfig: Config = {
  url: "https://api.example.com",
  timeout: 5000,
  retries: 3
};

// Atualização parcial: apenas timeout é alterado
const newConfig = updateConfig(defaultConfig, { timeout: 10000 });
// newConfig: { url: "https://api.example.com", timeout: 10000, retries: 3 }

Casos de uso comuns: formulários de edição que permitem alterar apenas alguns campos, estados de carregamento onde dados ainda não estão completos, e APIs de atualização parcial (PATCH).

3. Required: Tornando Propriedades Obrigatórias

Required<T> faz o oposto de Partial: remove o modificador ? de todas as propriedades, tornando-as obrigatórias. Útil para garantir que um objeto esteja completamente preenchido antes de ser processado.

interface UserInput {
  name?: string;
  email?: string;
  age?: number;
}

// Todas as propriedades agora são obrigatórias
type CompleteUser = Required<UserInput>;

function validateUser(user: Required<UserInput>): boolean {
  return user.name.length > 0 && user.email.includes("@") && user.age > 0;
}

// Erro de compilação: age está faltando
// validateUser({ name: "Ana", email: "ana@example.com" });

// Correto: todas as propriedades fornecidas
validateUser({ name: "Ana", email: "ana@example.com", age: 30 });

Contraste com Partial: Use Required quando a ausência de uma propriedade pode causar erros em tempo de execução. Partial é melhor para estados transitórios ou opcionais.

4. Pick: Selecionando Propriedades Específicas

Pick<T, K> cria um tipo contendo apenas as chaves especificadas em K (union de strings literais) do tipo T. Excelente para extrair subconjuntos de dados.

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  category: string;
  stock: number;
}

// Tipo apenas com campos de exibição em uma lista
type ProductPreview = Pick<Product, "id" | "name" | "price">;

function renderProductList(products: ProductPreview[]): void {
  products.forEach(p => {
    console.log(`${p.id}: ${p.name} - R$${p.price}`);
  });
}

const products: ProductPreview[] = [
  { id: 1, name: "Notebook", price: 4500 },
  { id: 2, name: "Mouse", price: 150 }
];

renderProductList(products);

Dica: Use Pick para criar tipos de resposta de API que retornam apenas campos relevantes para o cliente.

5. Omit: Excluindo Propriedades Específicas

Omit<T, K> remove as chaves especificadas em K do tipo T. É o complemento lógico de Pick. Ideal para ocultar dados sensíveis ou remover campos desnecessários.

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  token: string;
}

// Remove campos sensíveis
type PublicUser = Omit<User, "password" | "token">;

function getUserPublicData(user: User): PublicUser {
  const { password, token, ...publicData } = user;
  return publicData;
}

const user: User = {
  id: 42,
  name: "Carlos",
  email: "carlos@example.com",
  password: "secret123",
  token: "abc123"
};

const publicUser = getUserPublicData(user);
// publicUser: { id: 42, name: "Carlos", email: "carlos@example.com" }

Diferença fundamental: Pick seleciona o que incluir; Omit seleciona o que excluir. Escolha com base na clareza: se você quer manter 2 de 5 campos, use Pick; se quer remover 2 de 5, use Omit.

6. Record: Criando Tipos de Dicionário

Record<K, T> define um objeto onde as chaves são do tipo K (geralmente uma union de strings) e os valores são do tipo T. Perfeito para mapeamentos e configurações.

type Status = "success" | "error" | "pending";

interface StatusMessage {
  message: string;
  color: string;
}

// Mapeamento de status para mensagens
const statusMessages: Record<Status, StatusMessage> = {
  success: { message: "Operação concluída", color: "green" },
  error: { message: "Erro inesperado", color: "red" },
  pending: { message: "Aguardando processamento", color: "yellow" }
};

function getStatusMessage(status: Status): string {
  return statusMessages[status].message;
}

// Exemplo com enum e configurações
enum Environment {
  Development = "dev",
  Staging = "stg",
  Production = "prod"
}

type EnvConfig = Record<Environment, { apiUrl: string; debug: boolean }>;

const configs: EnvConfig = {
  [Environment.Development]: { apiUrl: "http://localhost:3000", debug: true },
  [Environment.Staging]: { apiUrl: "https://staging.api.com", debug: true },
  [Environment.Production]: { apiUrl: "https://api.com", debug: false }
};

7. Combinações e Composição de Tipos Utilitários

Os utilitários podem ser aninhados para criar tipos complexos com pouca verbosidade.

interface Task {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
}

// Tipo para formulário de edição: permite atualizar apenas título e descrição, ambos opcionais
type TaskEditForm = Partial<Pick<Task, "title" | "description">>;

// Tipo para criação: obrigatório, mas sem id e createdAt (gerados pelo sistema)
type TaskCreate = Required<Omit<Task, "id" | "createdAt">>;

function updateTask(id: number, updates: TaskEditForm): void {
  // Lógica de atualização
}

function createTask(task: TaskCreate): Task {
  return {
    id: Date.now(),
    createdAt: new Date(),
    ...task,
    completed: false
  };
}

// Uso com generics para maior flexibilidade
type EditableFields<T, K extends keyof T> = Partial<Pick<T, K>>;

// EditableFields<Task, "title" | "description"> = { title?: string; description?: string }

Benefício: Composições reduzem a duplicação de tipos e mantêm a consistência com o tipo base.

8. Boas Práticas e Armadilhas Comuns

Quando evitar o uso excessivo: Muitos utilitários aninhados podem prejudicar a legibilidade. Prefira criar tipos nomeados intermediários se a composição ficar complexa.

// Evite: muito aninhamento
type ComplexType = Partial<Required<Omit<Pick<T, "a" | "b">, "c">>>;

// Prefira: tipos intermediários com nomes descritivos
type BaseType = Pick<T, "a" | "b">;
type WithoutC = Omit<BaseType, "c">;
type FinalType = Partial<Required<WithoutC>>;

Limitações: Tipos utilitários não funcionam bem com tipos union complexos ou tipos condicionais. Por exemplo, Partial<string | number> não faz sentido. Sempre aplique utilitários sobre tipos de objeto.

Dicas para APIs públicas: Documente os utilitários usados em tipos exportados para que consumidores entendam as transformações. Evite expor tipos com muitos utilitários aninhados sem documentação.

Referências