Utility Types no TypeScript: Partial, Pick, Omit, Record

1. Visão Geral

Ao escrever TypeScript, você se depara com situações em que precisa de um tipo semelhante ao que já existe, mas faltam ou sobram alguns campos. Em vez de declarar uma nova interface, TypeScript fornece um conjunto de utility types para transformar tipos existentes. Este artigo aborda os quatro utility types mais usados: Partial, Pick, Omit e Record, com exemplos práticos e erros comuns. Ao terminar, você saberá escolher o utility type correto para cada situação em vez de reescrever interfaces manualmente.

2. Partial<T>

Partial<T> torna todos os campos de T opcionais. Útil ao escrever funções de atualização, onde quem chama precisa passar apenas alguns campos.

interface Customer {
  id: number;
  name: string;
  email: string;
  phone: string;
}

function updateCustomer(id: number, changes: Partial<Customer>): Customer {
  const current = findCustomerById(id); // assumindo que esta função já existe
  return { ...current, ...changes };
}

updateCustomer(1, { email: "new@mail.com" });

Partial<Customer> cria um tipo equivalente a:

interface PartialCustomer {
  id?: number;
  name?: string;
  email?: string;
  phone?: string;
}

Assim, updateCustomer aceita um objeto com apenas email sem reportar erro de campo faltante.

Observação: Partial torna opcional apenas no primeiro nível (shallow). Se Customer tem um campo que é um objeto aninhado, os campos dentro dele ainda são obrigatórios. Para deep partial, você precisa escrever um tipo recursivo customizado ou usar uma biblioteca como type-fest.

3. Pick<T, K>

Pick<T, K> seleciona um subconjunto de campos de T conforme a lista de chaves K. Use quando você precisa apenas de uma parte dos dados de um tipo grande, como um DTO a retornar ao cliente.

interface Order {
  id: string;
  customerId: number;
  totalAmount: number;
  items: string[];
  createdAt: Date;
  internalNote: string;
}

type OrderSummary = Pick<Order, "id" | "totalAmount" | "createdAt">;

const summary: OrderSummary = {
  id: "ORD-001",
  totalAmount: 250_000,
  createdAt: new Date(),
};

OrderSummary tem apenas 3 campos. Se você atribuir um campo items ou internalNote a summary, o compilador reportará um erro Object literal may only specify known properties.

3.1. Pick combinado com union de chaves

A chave passada para Pick é uma union de string literals, então você pode extrair para uma variável separada para reutilizar:

type OrderPublicFields = "id" | "totalAmount" | "createdAt";

type OrderSummary = Pick<Order, OrderPublicFields>;
type OrderListItem = Pick<Order, OrderPublicFields | "customerId">;

Isso evita repetir a lista de chaves em vários lugares quando você tem múltiplos DTOs variantes da mesma entidade.

4. Omit<T, K>

Omit<T, K> faz o oposto de Pick: remove as chaves especificadas e mantém o resto. Comumente usado quando você quer ocultar campos sensíveis ou internos.

interface User {
  id: number;
  username: string;
  passwordHash: string;
  role: string;
}

type PublicUser = Omit<User, "passwordHash">;

function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

A linha const { passwordHash, ...rest } = user separa passwordHash, rest contém apenas id, username, role — exatamente o que o tipo PublicUser define.

// Saída ao fazer console.log(toPublicUser(user))
{ id: 1, username: "nonla_dev", role: "ADMIN" }

Observação: Omit é definido com base em Pick e Exclude, funciona bem com tipos de objeto literal, mas não remove overloads ou discriminated unions corretamente em alguns casos complexos. Com union types (type A = B | C), Omit<A, K> vai fazer merge dos membros antes de omitir, podendo perder a propriedade discriminante. Ao encontrar unions complexas, considere escrever o tipo manualmente em vez de usar Omit diretamente.

5. Record<K, T>

Record<K, T> cria um tipo de objeto com todas as chaves pertencentes a K, cada chave com um valor do tipo T. Útil ao modelar um mapa/dicionário com chaves fixas.

type PaymentStatus = "PENDING" | "PAID" | "CANCELLED";

interface StatusInfo {
  label: string;
  color: string;
}

const statusDisplay: Record<PaymentStatus, StatusInfo> = {
  PENDING: { label: "Aguardando pagamento", color: "orange" },
  PAID: { label: "Pago", color: "green" },
  CANCELLED: { label: "Cancelado", color: "gray" },
};

function getStatusLabel(status: PaymentStatus): string {
  return statusDisplay[status].label;
}

Como statusDisplay é declarado como tipo Record<PaymentStatus, StatusInfo>, o compilador obriga você a ter todas as três chaves PENDING, PAID, CANCELLED. Se você esquecer uma chave, TypeScript reporta um erro no momento da compilação — essa é uma vantagem comparada ao usar { [key: string]: StatusInfo } comum (index signature não verifica se todas as chaves estão presentes).

5.1. Record com chaves dinâmicas

Quando a chave não é uma union literal, mas string genérico, Record<string, T> é equivalente a uma index signature:

const cache: Record<string, number> = {};
cache["order-1"] = 100;

Neste caso não há verificação de “chaves suficientes”, pois string tem infinitos valores. Use Record<string, T> apenas quando você realmente precisa de um dicionário livre, caso contrário prefira union literals para aproveitar a verificação exhaustiva.

6. Combinando múltiplos utility types

Os utility types podem ser aninhados para criar tipos mais complexos sem precisar escrever uma nova interface.

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

type ProductUpdateRequest = Partial<Omit<Product, "id">>;

function updateProduct(id: string, data: ProductUpdateRequest) {
  // chamando repository.update(id, data)
}

updateProduct("P001", { price: 199_000 });

Omit<Product, "id"> remove o campo id (não permite editar a chave primária), depois Partial torna os campos restantes opcionais. Resultado: quem chama precisa passar apenas os campos que quer alterar, não é permitido passar id.

7. Escrevendo testes para verificar o comportamento

Como utility types existem apenas em tempo de compilação, a maneira prática de verificar é escrever testes unitários para funções que usam esses tipos, garantindo que o comportamento em tempo de execução seja o esperado. Exemplo usando Vitest:

<!-- dependência do package.json equivalente (não é Maven) -->
import { describe, it, expect } from "vitest";

interface User {
  id: number;
  username: string;
  passwordHash: string;
  role: string;
}

type PublicUser = Omit<User, "passwordHash">;

function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

describe("toPublicUser", () => {
  it("remove passwordHash do resultado", () => {
    const user: User = {
      id: 1,
      username: "nonla_dev",
      passwordHash: "hash-123",
      role: "ADMIN",
    };

    const result = toPublicUser(user);

    expect(result).toEqual({
      id: 1,
      username: "nonla_dev",
      role: "ADMIN",
    });
    expect(result).not.toHaveProperty("passwordHash");
  });
});

Este teste confirma que a função toPublicUser não apenas tem o tipo correto, mas também o comportamento correto em tempo de execução — passwordHash realmente desaparece do objeto retornado, não apenas é “ocultado” na camada de tipo.

8. Erros comuns e quando não usar

  • Confundir Pick/Omit como remoção de dados real: ambos apenas mudam o tipo, não afetam o objeto em tempo de execução. Destructuring (const { x, ...rest } = obj) é o que realmente remove o campo dos dados.
  • Abuso de Partial para objetos retornados de API: se a resposta realmente sempre tem todos os campos, usar Partial vai forçar o código chamador a fazer verificações undefined desnecessárias em vários lugares. Use Partial apenas para input (update/patch), não para output que você sabe que está completo.
  • Omit com union types complexos: como dito na seção 4, Omit pode perder uma discriminated union. Se seu tipo é uma union com múltiplas variantes (por exemplo Shape = Circle | Square), escreva o tipo manualmente ou use técnicas de mapped types condicionais em vez de usar Omit diretamente.
  • Record<string, T> em vez de uma interface explícita: se você sabe de antemão todas as chaves, declare uma union literal e use Record<Key, T> para ter verificação exhaustiva. Usar Record<string, T> indiscriminadamente vai perder a capacidade de detectar chaves faltando ou extras no momento da compilação.

Observação: Todos esses utility types funcionam apenas em tempo de compilação. Após fazer build para JavaScript, eles desaparecem completamente — sem overhead em tempo de execução, mas também sem validação em tempo de execução. Se dados vêm de fora (API, arquivo JSON), você ainda precisa validar usando uma biblioteca como Zod ou io-ts.

9. Conclusão

Partial, Pick, Omit e Record ajudam você a reutilizar tipos em vez de reescrever interfaces duplicadas. Partial é adequado para input do tipo update, Pick/Omit são usados para criar DTOs a partir de entidades grandes, enquanto Record é ideal para dicionários com chaves fixas, aproveitando a verificação exhaustiva do compilador. Entender bem as limitações de cada tipo — especialmente o comportamento shallow de Partial e as questões de union type com Omit — vai ajudá-lo a evitar erros de tipo difíceis de debugar no futuro.

typescriptutility-typestype-systemfrontend