Union Types, Intersection Types e Type Guards em TypeScript: Do Básico ao Avançado Já leu

Union Types: Flexibilidade com Segurança Union Types permitem que uma variável ou parâmetro aceite múltiplos tipos diferentes. Em vez de forçar um único tipo, você declara que um valor pode ser de tipo A ou tipo B ou tipo C. Isso é fundamental quando você precisa trabalhar com dados que podem variar em sua natureza, mas ainda quer manter a segurança de tipos que TypeScript oferece. A sintaxe é simples: use o operador pipe ( ) entre os tipos. O compilador TypeScript garantirá que você só acesse propriedades e métodos comuns a todos os tipos na union, ou então execute verificações de tipo antes de usar funcionalidades específicas. Sem essa segurança, você poderia tentar chamar um método que existe apenas em um dos tipos, causando um erro em tempo de execução. Unions também funcionam com objetos complexos. Imagine um sistema que retorna dados diferentes baseado no tipo de requisição: Nome: ${pessoa.nome} Email: ${pessoa.email} Intersection Types: Combinação de Características Intersection Types

Union Types: Flexibilidade com Segurança

Union Types permitem que uma variável ou parâmetro aceite múltiplos tipos diferentes. Em vez de forçar um único tipo, você declara que um valor pode ser de tipo A ou tipo B ou tipo C. Isso é fundamental quando você precisa trabalhar com dados que podem variar em sua natureza, mas ainda quer manter a segurança de tipos que TypeScript oferece.

A sintaxe é simples: use o operador pipe (|) entre os tipos. O compilador TypeScript garantirá que você só acesse propriedades e métodos comuns a todos os tipos na union, ou então execute verificações de tipo antes de usar funcionalidades específicas. Sem essa segurança, você poderia tentar chamar um método que existe apenas em um dos tipos, causando um erro em tempo de execução.

type StatusResponse = 'sucesso' | 'erro' | 'pendente';

function procesarStatus(status: StatusResponse): void {
  if (status === 'sucesso') {
    console.log('Operação completada com sucesso');
  } else if (status === 'erro') {
    console.log('Ocorreu um erro durante a operação');
  } else {
    console.log('Ainda processando...');
  }
}

procesarStatus('sucesso'); // ✓ Válido
procesarStatus('invalido'); // ✗ Erro de compilação

Unions também funcionam com objetos complexos. Imagine um sistema que retorna dados diferentes baseado no tipo de requisição:

interface Usuario {
  tipo: 'usuario';
  nome: string;
  email: string;
}

interface Administrador {
  tipo: 'admin';
  nome: string;
  email: string;
  permissoes: string[];
}

type Pessoa = Usuario | Administrador;

function exibirPessoa(pessoa: Pessoa): void {
  console.log(`Nome: ${pessoa.nome}`);
  console.log(`Email: ${pessoa.email}`);

  // Propriedade 'permissoes' não existe em todas as unions
  // console.log(pessoa.permissoes); // ✗ Erro
}

Intersection Types: Combinação de Características

Intersection Types fazem o oposto das unions: em vez de "ou", temos "e". Um tipo intersection combina múltiplos tipos em um único tipo que possui todas as propriedades e métodos de cada um dos tipos combinados. Use o operador & para declarar intersections.

Isso é útil quando você precisa que um objeto cumpra contratos múltiplos simultaneamente. Por exemplo, um usuário que deve ser tanto uma Pessoa quanto um Funcionário. Em vez de duplicar código, você cria uma intersection que herda características de ambas as interfaces.

interface Pessoa {
  nome: string;
  idade: number;
}

interface Funcionario {
  matricula: string;
  salario: number;
  departamento: string;
}

type PessoaFuncionario = Pessoa & Funcionario;

const joao: PessoaFuncionario = {
  nome: 'João Silva',
  idade: 35,
  matricula: 'EMP001',
  salario: 5000,
  departamento: 'TI'
};

console.log(`${joao.nome} trabalha em ${joao.departamento}`);

Intersections são particularmente poderosas ao trabalhar com mixins e composição. Considere um cenário onde você precisa estender funcionalidade de uma classe base com comportamentos adicionais:

interface Auditável {
  criadoEm: Date;
  atualizadoEm: Date;
  criadoPor: string;
}

interface Deletável {
  deletadoEm?: Date;
  deletadoPor?: string;
}

interface Produto extends Auditável, Deletável {
  id: number;
  nome: string;
  preco: number;
}

const produto: Produto = {
  id: 1,
  nome: 'Notebook',
  preco: 3500,
  criadoEm: new Date('2024-01-01'),
  atualizadoEm: new Date('2024-01-15'),
  criadoPor: 'admin',
  deletadoEm: undefined,
  deletadoPor: undefined
};

Type Guards: Garantindo Segurança em Runtime

Type Guards são técnicas que permitem ao TypeScript (e ao seu código) identificar o tipo exato de uma variável em um determinado ponto. Quando você tem uma union de tipos, o compilador fica conservador: só permite operações que são seguras para todos os tipos da union. Type guards resolvem isso reduzindo o escopo de tipos possíveis através de verificações explícitas.

Verificação com typeof

O typeof operator é ideal para diferenciar tipos primitivos. Use-o quando sua union contém strings, números, booleans ou funções:

type Valor = string | number | boolean;

function procesarValor(valor: Valor): void {
  if (typeof valor === 'string') {
    console.log(`String em maiúscula: ${valor.toUpperCase()}`);
  } else if (typeof valor === 'number') {
    console.log(`Número dobrado: ${valor * 2}`);
  } else if (typeof valor === 'boolean') {
    console.log(`Booleano invertido: ${!valor}`);
  }
}

procesarValor('hello'); // String em maiúscula: HELLO
procesarValor(42); // Número dobrado: 84
procesarValor(true); // Booleano invertido: false

Verificação com instanceof

Use instanceof para verificar se um valor é instância de uma classe. Isso é essencial ao trabalhar com classes específicas em uma union:

class Cachorro {
  latir(): string {
    return 'Au au!';
  }
}

class Gato {
  miar(): string {
    return 'Miau!';
  }
}

type Animal = Cachorro | Gato;

function fazerSom(animal: Animal): void {
  if (animal instanceof Cachorro) {
    console.log(animal.latir());
  } else if (animal instanceof Gato) {
    console.log(animal.miar());
  }
}

fazerSom(new Cachorro()); // Au au!
fazerSom(new Gato()); // Miau!

Verificação com Propriedades Discriminantes

Quando você tem interfaces com uma propriedade comum que diferencia seus tipos (chamada discriminante), use-a para type narrowing. Essa é a abordagem mais elegante e performática:

interface Sucesso {
  tipo: 'sucesso';
  dados: unknown;
  mensagem: string;
}

interface Falha {
  tipo: 'falha';
  erro: Error;
  codigo: number;
}

type Resposta = Sucesso | Falha;

function tratarResposta(resposta: Resposta): void {
  if (resposta.tipo === 'sucesso') {
    console.log(`Sucesso: ${resposta.mensagem}`);
    console.log(`Dados: ${JSON.stringify(resposta.dados)}`);
  } else {
    console.log(`Erro ${resposta.codigo}: ${resposta.erro.message}`);
  }
}

const respostaBom: Resposta = {
  tipo: 'sucesso',
  dados: { id: 1, nome: 'Test' },
  mensagem: 'Operação concluída'
};

tratarResposta(respostaBom);

Type Predicates: Type Guards Customizados

Quando verificações simples não são suficientes, crie funções que retornam type predicates. A sintaxe is informa ao TypeScript que a função foi executada com sucesso, o tipo foi confirmado:

interface Configuracao {
  host: string;
  porta: number;
  ssl?: boolean;
}

interface Credenciais {
  usuario: string;
  senha: string;
}

type ConfigOuCredenciais = Configuracao | Credenciais;

function ehConfiguracao(obj: ConfigOuCredenciais): obj is Configuracao {
  return 'host' in obj && 'porta' in obj;
}

function ehCredenciais(obj: ConfigOuCredenciais): obj is Credenciais {
  return 'usuario' in obj && 'senha' in obj;
}

function conectar(config: ConfigOuCredenciais): void {
  if (ehConfiguracao(config)) {
    console.log(`Conectando em ${config.host}:${config.porta}`);
  } else if (ehCredenciais(config)) {
    console.log(`Autenticando usuário: ${config.usuario}`);
  }
}

const config: ConfigOuCredenciais = {
  host: 'localhost',
  porta: 3000,
  ssl: true
};

conectar(config);

Combinando Union, Intersection e Type Guards em Casos Práticos

A verdadeira maestria vem quando você combina essas três técnicas em um cenário real. Considere um sistema de processamento de eventos que precisa lidar com diferentes tipos de eventos, cada um com suas próprias propriedades:

// Definindo eventos diferentes como union
type Evento = EventoUsuario | EventoProduto | EventoPagamento;

interface EventoUsuario {
  tipo: 'usuario';
  acao: 'criacao' | 'atualizacao' | 'delecao';
  usuarioId: number;
  timestamp: Date;
}

interface EventoProduto {
  tipo: 'produto';
  acao: 'criacao' | 'atualizacao' | 'delecao';
  produtoId: number;
  estoque: number;
  timestamp: Date;
}

interface EventoPagamento {
  tipo: 'pagamento';
  acao: 'processado' | 'recusado' | 'pendente';
  valor: number;
  moeda: string;
  timestamp: Date;
}

// Type guard reutilizável
function ehEventoUsuario(evento: Evento): evento is EventoUsuario {
  return evento.tipo === 'usuario';
}

// Processador que combina tudo
function processarEvento(evento: Evento): void {
  // Verificação geral: propriedades comuns a todos
  console.log(`Evento processado em ${evento.timestamp}`);
  console.log(`Ação: ${evento.acao}`);

  // Type narrowing com discriminante
  if (ehEventoUsuario(evento)) {
    console.log(`Usuário ID: ${evento.usuarioId}`);
  } else if (evento.tipo === 'produto') {
    console.log(`Produto ID: ${evento.produtoId}`);
    console.log(`Estoque: ${evento.estoque}`);
  } else if (evento.tipo === 'pagamento') {
    console.log(`Valor: ${evento.valor} ${evento.moeda}`);
  }
}

// Testando
const eventoUser: Evento = {
  tipo: 'usuario',
  acao: 'criacao',
  usuarioId: 123,
  timestamp: new Date()
};

processarEvento(eventoUser);

Conclusão

Você aprendeu que Union Types permitem flexibilidade ao aceitar múltiplos tipos, mantendo segurança através de verificações explícitas. Intersection Types combinam características de múltiplas interfaces, permitindo composição elegante de comportamentos. Type Guards reduzem o escopo de tipos através de verificações de runtime, usando técnicas como typeof, instanceof, propriedades discriminantes e type predicates customizados. Quando combinadas, essas três abordagens criam código TypeScript robusto, legível e imune a erros de tipo em tempo de execução.

Referências


Artigos relacionados