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
- TypeScript Handbook: Declaration Merging — Documentação oficial completa sobre declaration merging, com exemplos detalhados de todos os tipos de merge
- TypeScript Deep Dive: Declaration Merging — Guia prático e aprofundado com casos de uso reais e explicações claras
- Merging Interfaces and Namespaces in TypeScript — Tutorial da DigitalOcean com exemplos práticos de merging entre interfaces e namespaces
- TypeScript: Declaration Merging Explained — Playground interativo oficial para experimentar declaration merging ao vivo
- TypeScript 5.x: Declaration Merging Best Practices — Tutorial com boas práticas, exemplos de código e explicações sobre limitações do declaration merging