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-transformer para transformação de objetos
  • Proxies: Proxy do 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