Decorators: metaprogramação com TypeScript
1. Introdução aos Decorators
Decorators são uma poderosa ferramenta de metaprogramação que permite anexar comportamentos e metadados a classes, métodos, propriedades e parâmetros em tempo de design. Em TypeScript, os decorators são uma proposta experimental (originalmente do ES7) que permite modificar ou estender o comportamento de elementos da linguagem sem alterar sua implementação original.
Historicamente, os decorators foram propostos para o ECMAScript, mas ainda não fazem parte do padrão oficial. No TypeScript, eles estão disponíveis desde a versão 1.5 como funcionalidade experimental. Com o TypeScript 5.x, os decorators continuam sendo suportados, mas exigem configuração específica.
Para habilitar decorators em seu projeto, adicione as seguintes opções no tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
2. Sintaxe e Funcionamento Básico
Um decorator é essencialmente uma função que recebe argumentos específicos dependendo do tipo de elemento ao qual é aplicado. A ordem de execução dos decorators é bottom-up: o decorator mais próximo do elemento é executado primeiro.
Vamos ver um exemplo básico de decorator de log:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Chamando ${propertyKey} com argumentos:`, args);
const result = originalMethod.apply(this, args);
console.log(`Resultado de ${propertyKey}:`, result);
return result;
};
return descriptor;
}
class Calculadora {
@log
somar(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculadora();
calc.somar(3, 4); // Log: Chamando somar com argumentos: [3, 4]
// Log: Resultado de somar: 7
3. Tipos de Decorators
Decorators de Classe
Modificam o construtor da classe ou adicionam funcionalidades ao protótipo:
function selavel<T extends { new (...args: any[]): {} }>(construtor: T) {
return class extends construtor {
constructor(...args: any[]) {
super(...args);
console.log(`Instância de ${construtor.name} criada`);
}
};
}
@selavel
class Usuario {
constructor(public nome: string) {}
}
new Usuario("João"); // Log: Instância de Usuario criada
Decorators de Método
Interceptam o PropertyDescriptor do método, permitindo modificar seu comportamento:
function medirTempo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(...args: any[]) {
const inicio = performance.now();
const resultado = metodoOriginal.apply(this, args);
const fim = performance.now();
console.log(`${propertyKey} executou em ${fim - inicio}ms`);
return resultado;
};
}
class Processador {
@medirTempo
processarDados(dados: number[]): number {
return dados.reduce((acc, val) => acc + val, 0);
}
}
Decorators de Propriedade
Observam definições de propriedades, mas não podem modificar diretamente seu valor:
function somenteLeitura(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false
});
}
class Configuracao {
@somenteLeitura
versao: string = "1.0.0";
}
Decorators de Parâmetro e Acessor
Decorators de parâmetro recebem o índice do parâmetro, enquanto decorators de acessor funcionam similarmente aos de método:
function validarParametro(target: Object, propertyKey: string | symbol, parameterIndex: number) {
console.log(`Parâmetro ${parameterIndex} de ${String(propertyKey)} decorado`);
}
class Servico {
executar(@validarParametro id: number, nome: string) {}
}
4. Fábricas de Decorators (Decorator Factories)
Fábricas de decorators permitem criar decorators parametrizáveis usando closures:
function log(nivel: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[${nivel}] ${propertyKey} chamado`);
return metodoOriginal.apply(this, args);
};
};
}
function timeout(ms: number) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(...args: any[]) {
return Promise.race([
metodoOriginal.apply(this, args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
)
]);
};
};
}
class ApiService {
@log('INFO')
@timeout(5000)
async buscarDados(url: string) {
return fetch(url).then(res => res.json());
}
}
5. Metadados e Reflexão com Decorators
O pacote reflect-metadata permite armazenar e recuperar metadados em tempo de design:
import "reflect-metadata";
function injetar(servicoId: string) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata("inject:service", servicoId, target, propertyKey);
};
}
class ServicoA {
executar() {
return "Serviço A executado";
}
}
class Cliente {
@injetar("ServicoA")
servico: ServicoA;
executar() {
const servicoId = Reflect.getMetadata("inject:service", this, "servico");
console.log(`Injetando serviço: ${servicoId}`);
}
}
Com emitDecoratorMetadata, o TypeScript automaticamente emite metadados de tipos:
function logTipo(target: any, propertyKey: string, parameterIndex: number) {
const tipoParametro = Reflect.getMetadata("design:paramtypes", target, propertyKey)[parameterIndex];
console.log(`Tipo do parâmetro ${parameterIndex}: ${tipoParametro.name}`);
}
class Exemplo {
metodo(@logTipo parametro: string) {}
}
6. Padrões Avançados com Decorators
Decorators de Validação
function validarPositivo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(valor: number) {
if (valor <= 0) {
throw new Error("Valor deve ser positivo");
}
return metodoOriginal.apply(this, [valor]);
};
}
class Conta {
@validarPositivo
depositar(valor: number): void {
console.log(`Depositando ${valor}`);
}
}
Decorators de Cache (Memoização)
function memoizar(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const cache = new Map();
const metodoOriginal = descriptor.value;
descriptor.value = function(...args: any[]) {
const chave = JSON.stringify(args);
if (cache.has(chave)) {
console.log("Cache hit!");
return cache.get(chave);
}
const resultado = metodoOriginal.apply(this, args);
cache.set(chave, resultado);
return resultado;
};
}
class Fibonacci {
@memoizar
calcular(n: number): number {
if (n <= 1) return n;
return this.calcular(n - 1) + this.calcular(n - 2);
}
}
Decorators de Autorização
function autorizar(...roles: string[]) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function(...args: any[]) {
const usuario = args[0]; // Assumindo que o primeiro argumento é o usuário
if (!roles.includes(usuario.role)) {
throw new Error("Acesso não autorizado");
}
return metodoOriginal.apply(this, args);
};
};
}
class AdminPanel {
@autorizar("admin", "superadmin")
excluirUsuario(usuario: any, id: number) {
console.log(`Usuário ${id} excluído`);
}
}
7. Limitações, Boas Práticas e Alternativas
Limitações
Decorators não podem modificar tipos em tempo de compilação — apenas comportamento em runtime. Isso significa que você não pode adicionar novas propriedades ou métodos ao tipo de uma classe usando decorators.
Problemas com herança: decorators aplicados a métodos de uma classe base podem não se comportar como esperado quando sobrescritos em subclasses.
Boas Práticas
- Use decorators para concerns transversais (logging, cache, autorização)
- Mantenha decorators simples e focados em uma única responsabilidade
- Documente claramente o comportamento esperado de cada decorator
- Evite decorators que modifiquem o fluxo de controle de forma imprevisível
Alternativas Modernas
- Class transformers: bibliotecas como
class-transformerpara transformação de objetos - Proxies:
Proxydo JavaScript para interceptar operações em objetos - Pattern Wrapper: funções que envolvem métodos manualmente
// Exemplo com Proxy como alternativa
function criarProxyComLog<T extends object>(obj: T): T {
return new Proxy(obj, {
get(target, prop, receiver) {
console.log(`Acessando propriedade ${String(prop)}`);
return Reflect.get(target, prop, receiver);
}
});
}
Quando Usar Decorators
- Quando você precisa de uma solução declarativa e reutilizável
- Em frameworks como Angular, NestJS ou TypeORM que dependem fortemente de decorators
- Para implementar padrões como Injeção de Dependência, validação e logging
Quando Evitar
- Em código que precisa ser altamente performático (decorators adicionam overhead)
- Quando a lógica pode ser implementada de forma mais simples sem metaprogramação
- Em projetos que precisam de máxima compatibilidade com JavaScript puro
Referências
- Documentação Oficial do TypeScript sobre Decorators — Guia completo sobre todos os tipos de decorators no TypeScript
- Decorators no ECMAScript Proposal — Proposta oficial do TC39 para decorators no JavaScript
- reflect-metadata no npm — Pacote oficial para metadados com decorators
- NestJS Decorators Guide — Tutorial prático de decorators customizados no framework NestJS
- TypeORM Decorators Documentation — Referência de decorators para mapeamento objeto-relacional
- Understanding TypeScript Decorators — Artigo técnico aprofundado sobre funcionamento e padrões com decorators
- Metaprogramming with TypeScript Decorators — Padrões avançados de metaprogramação usando decorators