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
- TypeScript Handbook: Utility Types — Documentação oficial da Microsoft com definições e exemplos de todos os tipos utilitários.
- TypeScript Deep Dive: Utility Types — Guia prático com explicações detalhadas e casos de uso avançados.
- Total TypeScript: Utility Types in TypeScript — Tutorial interativo com exercícios sobre Partial, Required, Pick, Omit e Record.
- Dev.to: Understanding TypeScript Utility Types — Artigo técnico com exemplos do mundo real e comparações entre utilitários.
- TypeScript Official Playground: Utility Types Examples — Sandbox oficial para testar tipos utilitários ao vivo no navegador.