Declaration merging: unindo interfaces e namespaces

1. O que é Declaration Merging e por que existe?

Declaration merging é um dos mecanismos mais poderosos do TypeScript. Ele permite que múltiplas declarações com o mesmo identificador sejam automaticamente combinadas em uma única definição. Diferente de JavaScript, onde redeclarar uma variável causa erro ou sobrescrita, o TypeScript inteligentemente mescla essas declarações.

Esse recurso existe para resolver problemas reais: estender bibliotecas de terceiros, adicionar propriedades a objetos globais, ou organizar código de forma modular sem perder a segurança de tipos.

// Duas interfaces com o mesmo nome são automaticamente unidas
interface Usuario {
  nome: string;
}

interface Usuario {
  idade: number;
}

// Resultado: Usuario tem { nome: string; idade: number }
const user: Usuario = {
  nome: "Alice",
  idade: 30
};

2. Merging de Interfaces: a forma mais comum

O merging de interfaces é o caso mais frequente e intuitivo. Quando você declara duas interfaces com o mesmo nome, suas propriedades se somam.

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

interface Produto {
  preco: number;
  categoria?: string;
}

// Uso normal
const livro: Produto = {
  id: 1,
  nome: "TypeScript na Prática",
  preco: 59.90,
  categoria: "Tecnologia"
};

Regras importantes:
- Propriedades com o mesmo nome devem ter tipos compatíveis (idênticos ou união)
- Propriedades opcionais podem coexistir com obrigatórias (a opcional prevalece)
- Métodos com mesma assinatura são fundidos; assinaturas diferentes viram sobrecarga

// Exemplo de conflito - isso gera erro!
interface Config {
  valor: string;
}

interface Config {
  valor: number; // Erro: tipos incompatíveis
}

Caso prático: estender tipos globais do navegador:

// Em um arquivo de declaração (.d.ts)
interface Window {
  email?: string;
}

// Agora podemos usar sem erro
window.email = "user@example.com";

3. Merging de Namespaces: combinando lógica e tipos

Namespaces com o mesmo nome também são fundidos. Isso é útil para organizar código e estender classes com membros estáticos.

class Validacao {
  static validarEmail(email: string): boolean {
    return email.includes('@');
  }
}

namespace Validacao {
  export function validarCPF(cpf: string): boolean {
    return cpf.length === 11;
  }

  export function validarTelefone(tel: string): boolean {
    return tel.length >= 10;
  }
}

// Uso
Validacao.validarEmail("teste@teste.com"); // método da classe
Validacao.validarCPF("12345678901");      // método do namespace

A classe Validacao ganha os métodos estáticos do namespace, criando um agrupamento lógico completo.

4. Merging de Interfaces com Namespaces: poder e cuidado

Quando uma interface e um namespace compartilham o mesmo nome, o TypeScript mescla os tipos da interface com os membros exportados do namespace.

interface MeuTipo {
  valor: string;
}

namespace MeuTipo {
  export function criar(valor: string): MeuTipo {
    return { valor };
  }

  export const padrao: MeuTipo = { valor: "default" };
}

// Uso
const item: MeuTipo = MeuTipo.criar("exemplo");
console.log(MeuTipo.padrao.valor); // "default"

Atenção: O escopo é crucial. Em módulos (com import/export), o comportamento pode diferir. Em arquivos .ts sem import/export, o escopo é global e o merge funciona naturalmente.

5. Merging de Funções e Namespaces: um padrão clássico

Funções também podem ser combinadas com namespaces. Isso permite adicionar propriedades diretamente à função, criando um padrão de "função com métodos auxiliares".

function criarUsuario(nome: string, idade: number) {
  return { nome, idade, tipo: "comum" as const };
}

namespace criarUsuario {
  export function admin(nome: string): { nome: string; tipo: "admin" } {
    return { nome, tipo: "admin" };
  }

  export const padrao = criarUsuario("Visitante", 0);
}

// Uso
const user1 = criarUsuario("João", 25);
const admin1 = criarUsuario.admin("Maria");
console.log(criarUsuario.padrao); // { nome: "Visitante", idade: 0, tipo: "comum" }

Comparação com sobrecarga de funções:

// Sobrecarga tradicional
function processar(x: number): number;
function processar(x: string): string;
function processar(x: any): any {
  return x;
}

// Merging com namespace - útil quando queremos propriedades na função
function processar(x: number): number {
  return x * 2;
}

namespace processar {
  export const versao = "1.0";
  export function log(x: number): void {
    console.log(`Processando: ${x}`);
  }
}

Use sobrecarga para variar parâmetros; use merging com namespace para adicionar propriedades e subfunções à função principal.

6. Limitações e Regras de Ouro do Declaration Merging

O que NÃO pode ser fundido:

// ❌ Type aliases não suportam merging
type MeuTipo = { a: number };
type MeuTipo = { b: string }; // Erro: identificador duplicado

// ❌ Enums não suportam merging (exceto em casos específicos com const)
enum Cor { Vermelho }
enum Cor { Azul } // Erro em strict mode

// ❌ Classes não podem ser fundidas entre si
class A { x: number = 0; }
class A { y: number = 0; } // Erro

Regras de visibilidade:
- Apenas membros exportados de namespaces são incluídos no merge
- Membros não exportados permanecem privados ao namespace original

Boas práticas:
- Prefira extends para estender interfaces quando possível
- Use merging para estender bibliotecas de terceiros ou tipos globais
- Evite merges excessivos que dificultem a legibilidade do código

7. Casos Avançados: Merging em Módulos e Bibliotecas

Em módulos (arquivos com import/export), o comportamento muda. Para estender tipos globais a partir de um módulo, use declare global:

// Em um arquivo .ts com import/export
export {};

declare global {
  interface String {
    toTitleCase(): string;
  }
}

// Implementação
String.prototype.toTitleCase = function() {
  return this.replace(/\w\S*/g, 
    txt => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase()
  );
};

Exemplo real: estender Array com método personalizado:

// tipos.d.ts
interface Array<T> {
  primeiro(): T | undefined;
  ultimo(): T | undefined;
}

// implementacao.ts
Array.prototype.primeiro = function<T>() {
  return this.length > 0 ? this[0] : undefined;
};

Array.prototype.ultimo = function<T>() {
  return this.length > 0 ? this[this.length - 1] : undefined;
};

// Uso
const nums = [1, 2, 3];
console.log(nums.primeiro()); // 1
console.log(nums.ultimo());   // 3

Cuidado: Estender tipos globais como String ou Array pode poluir o escopo global e causar conflitos em projetos grandes. Use com moderação e documente claramente.


O declaration merging é uma ferramenta poderosa do TypeScript que, quando usada corretamente, permite estender tipos de forma elegante e segura. A chave é entender suas regras e limitações para evitar armadilhas e manter a clareza do código.

Referências