O que é uma Interface em TypeScript?
Uma interface em TypeScript é um contrato que define a estrutura de um objeto. Ela descreve quais propriedades e métodos um objeto deve ter, sem implementação — apenas a assinatura. Pense em uma interface como um molde ou blueprint: você a define, e qualquer classe ou objeto que "implemente" essa interface deve seguir as regras estabelecidas.
A grande vantagem é o type checking em tempo de desenvolvimento. O TypeScript verifica se você está usando o objeto corretamente antes mesmo de executar o código. Isso reduz bugs e torna seu código mais seguro e previsível. Diferente de classes, interfaces não existem em tempo de execução — o compilador as remove durante a transpilação para JavaScript.
// Definindo uma interface simples
interface Usuario {
id: number;
nome: string;
email: string;
ativo: boolean;
}
// Criando um objeto que segue a interface
const usuario: Usuario = {
id: 1,
nome: "João Silva",
email: "joao@example.com",
ativo: true
};
// Se tentar atribuir um objeto incompleto, TypeScript gera erro
const usuarioIncompleto: Usuario = {
id: 2,
nome: "Maria"
// ❌ Erro: propriedades 'email' e 'ativo' não foram atribuídas
};
Definição Avançada e Propriedades Opcionais
Interfaces podem incluir propriedades opcionais e de apenas leitura, proporcionando controle fino sobre a estrutura esperada. Uma propriedade opcional é marcada com ?, indicando que o objeto não precisa obrigatoriamente fornecê-la. Propriedades de apenas leitura usam readonly, garantindo que não serão modificadas após a criação.
Também é possível definir assinaturas de métodos dentro da interface, especificando qual o retorno esperado e quais parâmetros o método deve aceitar. Isso cria contratos mais robustos quando você precisa garantir comportamentos específicos em classes ou funções que implementam a interface.
interface Produto {
readonly id: number; // Não pode ser modificado
nome: string;
preco: number;
descricao?: string; // Opcional
tags?: string[]; // Array opcional
calcularDesconto(percentual: number): number; // Método
}
class ProdutoFisico implements Produto {
readonly id: number;
nome: string;
preco: number;
descricao?: string;
tags?: string[];
constructor(id: number, nome: string, preco: number) {
this.id = id;
this.nome = nome;
this.preco = preco;
}
calcularDesconto(percentual: number): number {
return this.preco * (1 - percentual / 100);
}
}
const produto = new ProdutoFisico(1, "Notebook", 3000);
produto.nome = "Notebook Gamer"; // ✅ Permitido
// produto.id = 2; // ❌ Erro: não pode atribuir a propriedade 'readonly'
console.log(produto.calcularDesconto(10)); // 2700
Extensão de Interfaces
A extensão de interfaces permite reutilizar e construir sobre interfaces já existentes, promovendo código DRY (Don't Repeat Yourself). Usando a palavra-chave extends, você cria uma nova interface que herda todas as propriedades e métodos de uma ou mais interfaces existentes, adicionando novas propriedades conforme necessário.
Uma interface pode estender múltiplas outras interfaces simultaneamente, criando estruturas complexas a partir de componentes menores e reutilizáveis. Isso é particularmente útil em aplicações grandes onde você tem conceitos base que são expandidos em diferentes contextos. A extensão também permite sobrescrever tipos: uma propriedade pode ser redefinida em uma interface derivada com um tipo mais específico.
// Interface base
interface Entidade {
readonly id: number;
dataCriacao: Date;
}
// Interface que estende Entidade
interface Usuario extends Entidade {
nome: string;
email: string;
verificarEmail(): Promise<boolean>;
}
// Interface que estende múltiplas interfaces
interface AdminDeSistema extends Usuario {
nivelAcesso: "admin" | "moderador" | "usuario";
permissoes: string[];
revogarPermissao(permissao: string): void;
}
// Implementação concreta
class Admin implements AdminDeSistema {
readonly id: number;
dataCriacao: Date;
nome: string;
email: string;
nivelAcesso: "admin" | "moderador" | "usuario";
permissoes: string[];
constructor(
id: number,
nome: string,
email: string,
nivelAcesso: "admin" | "moderador" | "usuario"
) {
this.id = id;
this.dataCriacao = new Date();
this.nome = nome;
this.email = email;
this.nivelAcesso = nivelAcesso;
this.permissoes = [];
}
async verificarEmail(): Promise<boolean> {
// Implementação da verificação
return true;
}
revogarPermissao(permissao: string): void {
this.permissoes = this.permissoes.filter(p => p !== permissao);
}
}
const admin = new Admin(1, "Carlos", "carlos@admin.com", "admin");
admin.permissoes = ["criar_usuarios", "deletar_usuarios"];
admin.revogarPermissao("deletar_usuarios");
console.log(admin.permissoes); // ["criar_usuarios"]
Declaration Merging em Interfaces
Declaration merging (fusão de declarações) é um recurso exclusivo de TypeScript que permite declarar a mesma interface múltiplas vezes, e o compilador as combina automaticamente em uma única interface. Isso é especialmente útil quando você quer estender funcionalidades de tipos existentes ou quando trabalha com código que será integrado de múltiplas fontes.
Quando você declara a mesma interface duas vezes, as propriedades de ambas as declarações são combinadas. Isso funciona apenas com interfaces — classes e tipos não suportam merging. Um caso de uso prático é estender interfaces de bibliotecas externas sem modificar o código original, ou quando você tem múltiplos arquivos que constroem a mesma interface incrementalmente.
// Primeira declaração da interface
interface Configuracao {
apiUrl: string;
timeout: number;
}
// Segunda declaração — será mesclada automaticamente
interface Configuracao {
debug: boolean;
maxRetries: number;
}
// TypeScript trata como se fosse:
// interface Configuracao {
// apiUrl: string;
// timeout: number;
// debug: boolean;
// maxRetries: number;
// }
const config: Configuracao = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: true,
maxRetries: 3
};
console.log(config); // ✅ Todas as propriedades são obrigatórias
Merging com Namespaces
Declaration merging também funciona ao combinar interfaces com namespaces, permitindo organizar código em módulos lógicos. Essa técnica é comum quando você quer separar tipos relacionados em diferentes espaços de nome para melhor organização.
// Declarar um namespace com uma interface
namespace Configuracao {
export interface Banco {
host: string;
porta: number;
usuario: string;
senha: string;
}
}
// Declarar uma interface no escopo global com o mesmo nome
interface Configuracao {
ambiente: "producao" | "desenvolvimento";
}
// Agora você acessa ambas:
const configBanco: Configuracao.Banco = {
host: "localhost",
porta: 5432,
usuario: "admin",
senha: "senha123"
};
const config: Configuracao = {
ambiente: "desenvolvimento"
};
Estendendo Tipos Globais
Um caso de uso prático do declaration merging é estender interfaces globais fornecidas por bibliotecas, como estender o objeto Window no navegador ou adicionar propriedades ao objeto process do Node.js.
// Estendendo a interface Window do navegador
declare global {
interface Window {
meuApp: {
versao: string;
inicializar(): void;
};
}
}
// Agora você pode usar:
window.meuApp = {
versao: "1.0.0",
inicializar() {
console.log("Aplicação iniciada");
}
};
// Sem declaration merging, TypeScript reclamaria que
// 'meuApp' não existe em Window
Conclusão
Interfaces em TypeScript são muito mais que simples documentação: são contratos que garantem segurança de tipo e melhor manutenibilidade. A capacidade de estender interfaces permite construir hierarquias de tipos reutilizáveis e escaláveis, enquanto o declaration merging oferece flexibilidade para integrar e estender código de múltiplas fontes sem duplicação. Dominando esses três pilares — definição, extensão e merging — você terá ferramentas poderosas para escrever código TypeScript profissional e resiliente.