Boas Práticas de Tipos Avançados em TypeScript: Union, Intersection, Generics e Utility Types para Times Ágeis Já leu

Union Types: Combinando Múltiplos Tipos Union types permitem que uma variável aceite múltiplos tipos específicos. É um dos mecanismos mais práticos para criar APIs flexíveis sem perder segurança de tipos. Use a sintaxe de barra vertical (|) para declarar um union type. A vantagem emerge ao usar type guards para estreitar o tipo em tempo de execução. O TypeScript então oferece autocompletar específico para cada tipo. Padrões como , e discriminated unions (usando propriedades literais) garantem código robusto e legível. Intersection Types: Combinando Propriedades Intersection types (& operador) mesclam múltiplos tipos em um único tipo que possui todas as propriedades de ambos. É diferente de union: aqui o valor deve satisfazer todos os tipos simultaneamente, não apenas um. Intersection types são especialmente úteis para composição de tipos e para adicionar capacidades específicas a tipos existentes. Use-os quando precisar garantir que um objeto cumpra múltiplos contratos simultaneamente. Evite uso excessivo, pois tornam assinaturas complexas; prefira composição com interfaces quando apropriado. Generics:

Union Types: Combinando Múltiplos Tipos

Union types permitem que uma variável aceite múltiplos tipos específicos. É um dos mecanismos mais práticos para criar APIs flexíveis sem perder segurança de tipos. Use a sintaxe de barra vertical (|) para declarar um union type.

type Status = "pending" | "approved" | "rejected";
type Result = string | number;

function processPayment(status: Status): void {
  if (status === "pending") {
    console.log("Processando pagamento...");
  }
}

const userId: Result = 42; // válido
const userName: Result = "João"; // válido
// const invalid: Result = true; // erro

A vantagem emerge ao usar type guards para estreitar o tipo em tempo de execução. O TypeScript então oferece autocompletar específico para cada tipo. Padrões como typeof, instanceof e discriminated unions (usando propriedades literais) garantem código robusto e legível.

type Response = { success: true; data: string } | { success: false; error: string };

function handleResponse(res: Response): void {
  if (res.success) {
    console.log(res.data); // tipo: string
  } else {
    console.log(res.error); // tipo: string
  }
}

Intersection Types: Combinando Propriedades

Intersection types (& operador) mesclam múltiplos tipos em um único tipo que possui todas as propriedades de ambos. É diferente de union: aqui o valor deve satisfazer todos os tipos simultaneamente, não apenas um.

interface Funcionario {
  nome: string;
  salario: number;
}

interface Gerente {
  departamento: string;
  equipe: number;
}

type GerenteFuncionario = Funcionario & Gerente;

const gerente: GerenteFuncionario = {
  nome: "Maria",
  salario: 5000,
  departamento: "TI",
  equipe: 5
};

Intersection types são especialmente úteis para composição de tipos e para adicionar capacidades específicas a tipos existentes. Use-os quando precisar garantir que um objeto cumpra múltiplos contratos simultaneamente. Evite uso excessivo, pois tornam assinaturas complexas; prefira composição com interfaces quando apropriado.

type Auditavel = { criado: Date; atualizado: Date };
type Validavel = { validar(): boolean };

type Documento = { titulo: string } & Auditavel & Validavel;

const doc: Documento = {
  titulo: "Contrato",
  criado: new Date(),
  atualizado: new Date(),
  validar() { return true; }
};

Generics: Reutilização Parametrizada de Tipos

Generics são variáveis de tipo que tornam componentes reutilizáveis enquanto mantêm segurança estática. São fundamentais em TypeScript profissional. Declare um generic com entre chevrons, onde T é convenção para "Type".

function obterPrimeiro<T>(lista: T[]): T {
  return lista[0];
}

const numeros = obterPrimeiro([1, 2, 3]); // tipo: number
const nomes = obterPrimeiro(["Ana", "Bruno"]); // tipo: string

// Generics com constraints
function obterPropriedade<T, K extends keyof T>(obj: T, chave: K): T[K] {
  return obj[chave];
}

const pessoa = { nome: "João", idade: 30 };
const idade = obterPropriedade(pessoa, "idade"); // tipo: number
// obterPropriedade(pessoa, "email"); // erro: propriedade inexistente

Generics com constraints (extends) limitam quais tipos podem ser passados. Use keyof T para tipos seguros de propriedades. Para casos avançados, combine com unions e intersections. Generics em classes modelam estruturas genéricas como pilhas, filas e repositórios sem duplicação de código.

class Repositorio<T> {
  private dados: T[] = [];

  adicionar(item: T): void {
    this.dados.push(item);
  }

  obter(indice: number): T | undefined {
    return this.dados[indice];
  }

  listar(): T[] {
    return [...this.dados];
  }
}

interface Usuario {
  id: number;
  email: string;
}

const repoUsuarios = new Repositorio<Usuario>();
repoUsuarios.adicionar({ id: 1, email: "user@example.com" });
const usuario = repoUsuarios.obter(0); // tipo: Usuario | undefined

Utility Types: Transformações Prontas

TypeScript fornece utility types nativos que transformam tipos existentes de forma poderosa. São verdadeiros multiplicadores de produtividade.

Principais Utility Types

Partial torna todas as propriedades opcionais; Required faz o oposto. Pick seleciona propriedades específicas; Omit remove propriedades. Record cria objetos com chaves conhecidas. Readonly torna propriedades imutáveis.

interface Produto {
  id: number;
  nome: string;
  preco: number;
  descricao: string;
}

// Partial: todas propriedades opcionais para atualização
type AtualizarProduto = Partial<Produto>;

// Pick: apenas nome e preco para exibição
type ProdutoResumido = Pick<Produto, "nome" | "preco">;

// Omit: tudo exceto id (para criação)
type CriarProduto = Omit<Produto, "id">;

// Record: mapa de categorias
type CategoriaInventario = Record<"eletronicos" | "livros" | "roupas", number>;
const estoque: CategoriaInventario = {
  eletronicos: 50,
  livros: 120,
  roupas: 200
};

// Readonly: preço não pode ser modificado
type ProdutoImutavel = Readonly<Produto>;

Extract extrai tipos que correspondem a U de T; Exclude faz o oposto. ReturnType obtém o tipo de retorno de uma função. Parameters extrai os tipos de parâmetros. Estes são indispensáveis em código metaprogramado e em bibliotecas robustas.

// Extract e Exclude
type Status = "ativo" | "inativo" | "pendente" | "cancelado";
type StatusAtivos = Exclude<Status, "cancelado">; // "ativo" | "inativo" | "pendente"
type ApenasAtivo = Extract<Status, "ativo">; // "ativo"

// ReturnType e Parameters
function autenticar(usuario: string, senha: string): { token: string } {
  return { token: "xyz" };
}

type RetornoAuth = ReturnType<typeof autenticar>; // { token: string }
type ParamsAuth = Parameters<typeof autenticar>; // [string, string]

Conclusão

Três aprendizados principais consolidam domínio sobre tipos avançados em TypeScript. Primeiro, unions e intersections são ferramentas complementares: use unions para alternativas e intersections para composição. Segundo, generics são essenciais para código reutilizável que mantém segurança de tipos; não tenha medo de combiná-los com constraints. Terceiro, utility types são multiplicadores de produtividade que evitam duplicação; domine os comuns (Partial, Pick, Omit, Record) e explore avançados conforme necessário. Aplicar estes conceitos transforma código em sistemas mais seguros, inteligíveis e mantíveis.

Referências


Artigos relacionados