Português

Explore o poder dos Decoradores TypeScript para programação com metadados, programação orientada a aspectos e aprimoramento de código com padrões declarativos. Um guia abrangente para desenvolvedores globais.

Decoradores TypeScript: Dominando Padrões de Programação com Metadados para Aplicações Robustas

Na vasta paisagem do desenvolvimento de software moderno, manter bases de código limpas, escaláveis e gerenciáveis é primordial. O TypeScript, com seu poderoso sistema de tipos e recursos avançados, fornece aos desenvolvedores ferramentas para alcançar isso. Entre seus recursos mais intrigantes e transformadores estão os Decoradores. Embora ainda seja um recurso experimental no momento da escrita (proposta de estágio 3 para ECMAScript), os decoradores são amplamente utilizados em frameworks como Angular e TypeORM, mudando fundamentalmente a forma como abordamos padrões de design, programação com metadados e programação orientada a aspectos (AOP).

Este guia abrangente investigará profundamente os decoradores TypeScript, explorando sua mecânica, vários tipos, aplicações práticas e melhores práticas. Se você está construindo aplicações empresariais em larga escala, microsserviços ou interfaces web do lado do cliente, entender os decoradores o capacitará a escrever código TypeScript mais declarativo, mantenível e poderoso.

Entendendo o Conceito Central: O que é um Decorador?

Em sua essência, um decorador é um tipo especial de declaração que pode ser anexado a uma declaração de classe, método, acessador, propriedade ou parâmetro. Decoradores são funções que retornam um novo valor (ou modificam um existente) para o alvo que estão decorando. Seu propósito principal é adicionar metadados ou alterar o comportamento da declaração à qual estão anexados, sem modificar diretamente a estrutura do código subjacente. Essa maneira externa e declarativa de aumentar o código é incrivelmente poderosa.

Pense em decoradores como anotações ou rótulos que você aplica a partes do seu código. Esses rótulos podem então ser lidos ou utilizados por outras partes do seu aplicativo ou por frameworks, muitas vezes em tempo de execução, para fornecer funcionalidade ou configuração adicionais.

A Sintaxe de um Decorador

Decoradores são prefixados com o símbolo @, seguido pelo nome da função decoradora. Eles são colocados imediatamente antes da declaração que estão decorando.

@MeuDecorador
class MinhaClasse {
  @OutroDecorador
  meuMetodo() {
    // ...
  }
}

Habilitando Decoradores em TypeScript

Antes de poder usar decoradores, você deve habilitar a opção do compilador experimentalDecorators em seu arquivo tsconfig.json. Além disso, para recursos avançados de reflexão de metadados (frequentemente usados por frameworks), você também precisará de emitDecoratorMetadata e do polyfill reflect-metadata.

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2017",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Você também precisa instalar reflect-metadata:

npm install reflect-metadata --save
# ou
yarn add reflect-metadata

E importá-lo no topo do ponto de entrada da sua aplicação (por exemplo, main.ts ou app.ts):

import "reflect-metadata";
// O código da sua aplicação segue

Fábricas de Decoradores: Customização ao Seu Alcance

Embora um decorador básico seja uma função, muitas vezes você precisará passar argumentos para um decorador para configurar seu comportamento. Isso é alcançado usando uma fábrica de decoradores. Uma fábrica de decoradores é uma função que retorna a função decoradora real. Quando você aplica uma fábrica de decoradores, você a chama com seus argumentos, e ela retorna a função decoradora que o TypeScript aplica ao seu código.

Criando um Exemplo Simples de Fábrica de Decoradores

Vamos criar uma fábrica para um decorador Logger que pode registrar mensagens com diferentes prefixos.

function Logger(prefix: string) {
  return function (target: Function) {
    console.log(`[${prefix}] Classe ${target.name} foi definida.`);
  };
}

@Logger("APP_INIT")
class ApplicationBootstrap {
  constructor() {
    console.log("Aplicação iniciando...");
  }
}

const app = new ApplicationBootstrap();
// Saída:
// [APP_INIT] Classe ApplicationBootstrap foi definida.
// Aplicação iniciando...

Neste exemplo, Logger("APP_INIT") é a chamada da fábrica de decoradores. Ela retorna a função decoradora real que recebe target: Function (o construtor da classe) como seu argumento. Isso permite a configuração dinâmica do comportamento do decorador.

Tipos de Decoradores em TypeScript

O TypeScript suporta cinco tipos distintos de decoradores, cada um aplicável a um tipo específico de declaração. A assinatura da função decoradora varia com base no contexto em que é aplicada.

1. Decoradores de Classe

Decoradores de classe são aplicados a declarações de classe. A função decoradora recebe o construtor da classe como seu único argumento. Um decorador de classe pode observar, modificar ou até mesmo substituir uma definição de classe.

Assinatura:

function DecoradorDeClasse(target: Function) { ... }

Valor de Retorno:

Se o decorador de classe retornar um valor, ele substituirá a declaração da classe pela função construtora fornecida. Este é um recurso poderoso, frequentemente usado para mixins ou aumento de classe. Se nenhum valor for retornado, a classe original é usada.

Casos de Uso:

Exemplo de Decorador de Classe: Injetando um Serviço

Imagine um cenário simples de injeção de dependência onde você deseja marcar uma classe como "injetável" e, opcionalmente, fornecer um nome para ela em um contêiner.

const InjectableServiceRegistry = new Map<string, Function>();

function Injectable(name?: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const serviceName = name || constructor.name;
    InjectableServiceRegistry.set(serviceName, constructor);
    console.log(`Serviço registrado: ${serviceName}`);

    // Opcionalmente, você poderia retornar uma nova classe aqui para aumentar o comportamento
    return class extends constructor {
      createdAt = new Date();
      // Propriedades ou métodos adicionais para todos os serviços injetados
    };
  };
}

@Injectable("UserService")
class UserDataService {
  getUsers() {
    return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
  }
}

@Injectable()
class ProductDataService {
  getProducts() {
    return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
  }
}

console.log("--- Serviços Registrados ---");
console.log(Array.from(InjectableServiceRegistry.keys()));

const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
  const userServiceInstance = new userServiceConstructor();
  console.log("Usuários:", userServiceInstance.getUsers());
  // console.log("UserService Created At:", userServiceInstance.createdAt); // Se a classe retornada for usada
}

Este exemplo demonstra como um decorador de classe pode registrar uma classe e até modificar seu construtor. O decorador Injectable torna a classe detectável por um sistema de injeção de dependência hipotético.

2. Decoradores de Método

Decoradores de método são aplicados a declarações de método. Eles recebem três argumentos: o objeto alvo (para membros estáticos, a função construtora; para membros de instância, o protótipo da classe), o nome do método e o descritor de propriedade do método.

Assinatura:

function DecoradorDeMetodo(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Valor de Retorno:

Um decorador de método pode retornar um novo PropertyDescriptor. Se o fizer, esse descritor será usado para definir o método. Isso permite modificar ou substituir a implementação original do método, tornando-o incrivelmente poderoso para AOP.

Casos de Uso:

Exemplo de Decorador de Método: Monitoramento de Desempenho

Vamos criar um decorador MeasurePerformance para registrar o tempo de execução de um método.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    const start = process.hrtime.bigint();
    const result = originalMethod.apply(this, args);
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1_000_000;
    console.log(`Método "${propertyKey}" executado em ${duration.toFixed(2)} ms`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @MeasurePerformance
  processData(data: number[]): number[] {
    // Simula uma operação complexa e demorada
    for (let i = 0; i < 1_000_000; i++) {
      Math.sin(i);
    }
    return data.map(n => n * 2);
  }

  @MeasurePerformance
  fetchRemoteData(id: string): Promise<string> {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(`Dados para ID: ${id}`);
      }, 500);
    });
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));

O decorador MeasurePerformance envolve o método original com lógica de tempo, imprimindo a duração da execução sem poluir a lógica de negócios dentro do próprio método. Este é um exemplo clássico de Programação Orientada a Aspectos (AOP).

3. Decoradores de Acessador

Decoradores de acessador são aplicados a declarações de acessador (get e set). Semelhante aos decoradores de método, eles recebem o objeto alvo, o nome do acessador e seu descritor de propriedade.

Assinatura:

function DecoradorDeAcessador(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Valor de Retorno:

Um decorador de acessador pode retornar um novo PropertyDescriptor, que será usado para definir o acessador.

Casos de Uso:

Exemplo de Decorador de Acessador: Cache de Getters

Vamos criar um decorador que armazena em cache o resultado de uma computação cara de getter.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;
  const cacheKey = `_cached_${String(propertyKey)}`;

  if (originalGetter) {
    descriptor.get = function() {
      if (this[cacheKey] === undefined) {
        console.log(`[Cache Miss] Calculando valor para ${String(propertyKey)}`);
        this[cacheKey] = originalGetter.apply(this);
      } else {
        console.log(`[Cache Hit] Usando valor em cache para ${String(propertyKey)}`);
      }
      return this[cacheKey];
    };
  }
  return descriptor;
}

class ReportGenerator {
  private data: number[];

  constructor(data: number[]) {
    this.data = data;
  }

  // Simula uma computação cara
  @CachedGetter
  get expensiveSummary(): number {
    console.log("Executando cálculo de resumo caro...");
    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
  }
}

const generator = new ReportGenerator([10, 20, 30, 40, 50]);

console.log("Primeiro acesso:", generator.expensiveSummary);
console.log("Segundo acesso:", generator.expensiveSummary);
console.log("Terceiro acesso:", generator.expensiveSummary);

Este decorador garante que a computação do getter expensiveSummary seja executada apenas uma vez; chamadas subsequentes retornam o valor em cache. Este padrão é muito útil para otimizar o desempenho onde o acesso à propriedade envolve computação pesada ou chamadas externas.

4. Decoradores de Propriedade

Decoradores de propriedade são aplicados a declarações de propriedade. Eles recebem dois argumentos: o objeto alvo (para membros estáticos, a função construtora; para membros de instância, o protótipo da classe) e o nome da propriedade.

Assinatura:

function DecoradorDePropriedade(target: Object, propertyKey: string | symbol) { ... }

Valor de Retorno:

Decoradores de propriedade não podem retornar nenhum valor. Seu uso principal é registrar metadados sobre a propriedade. Eles não podem alterar diretamente o valor da propriedade ou seu descritor no momento da decoração, pois o descritor de uma propriedade ainda não está totalmente definido quando os decoradores de propriedade são executados.

Casos de Uso:

Exemplo de Decorador de Propriedade: Validação de Campo Obrigatório

Vamos criar um decorador para marcar uma propriedade como "obrigatória" e, em seguida, validá-la em tempo de execução.

interface ValidationRule {
  property: string | symbol;
  validate: (value: any) => boolean;
  message: string;
}

const validationRules: Map<Function, ValidationRule[]> = new Map();

function Required(target: Object, propertyKey: string | symbol) {
  const rules = validationRules.get(target.constructor) || [];
  rules.push({
    property: propertyKey,
    validate: (value: any) => value !== null && value !== undefined && value !== "",
    message: `${String(propertyKey)} é obrigatório.`
  });
  validationRules.set(target.constructor, rules);
}

function validate(instance: any): string[] {
  const classRules = validationRules.get(instance.constructor) || [];
  const errors: string[] = [];

  for (const rule of classRules) {
    if (!rule.validate(instance[rule.property])) {
      errors.push(rule.message);
    }
  }
  return errors;
}

class UserProfile {
  @Required
  firstName: string;

  @Required
  lastName: string;

  age?: number;

  constructor(firstName: string, lastName: string, age?: number) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

const user1 = new UserProfile("John", "Doe", 30);
console.log("Validação do Usuário 1 erros:", validate(user1)); // []

const user2 = new UserProfile("", "Smith");
console.log("Validação do Usuário 2 erros:", validate(user2)); // ["firstName é obrigatório."]

const user3 = new UserProfile("Alice", "");
console.log("Validação do Usuário 3 erros:", validate(user3)); // ["lastName é obrigatório."]

O decorador Required simplesmente registra a regra de validação com um mapa central validationRules. Uma função validate separada usa então esses metadados para verificar a instância em tempo de execução. Este padrão separa a lógica de validação da definição de dados, tornando-a reutilizável e limpa.

5. Decoradores de Parâmetro

Decoradores de parâmetro são aplicados a parâmetros dentro de um construtor de classe ou de um método. Eles recebem três argumentos: o objeto alvo (para membros estáticos, a função construtora; para membros de instância, o protótipo da classe), o nome do método (ou undefined para parâmetros do construtor) e o índice ordinal do parâmetro na lista de parâmetros da função.

Assinatura:

function DecoradorDeParametro(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Valor de Retorno:

Decoradores de parâmetro não podem retornar nenhum valor. Semelhante aos decoradores de propriedade, seu papel principal é adicionar metadados sobre o parâmetro.

Casos de Uso:

Exemplo de Decorador de Parâmetro: Injetando Dados de Requisição

Vamos simular como um framework web pode usar decoradores de parâmetro para injetar dados específicos em um parâmetro de método, como um ID de usuário de uma requisição.

interface ParameterMetadata {
  index: number;
  key: string | symbol;
  resolver: (request: any) => any;
}

const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();

function RequestParam(paramName: string) {
  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
    const targetKey = propertyKey || "constructor";
    let methodResolvers = parameterResolvers.get(target.constructor);
    if (!methodResolvers) {
      methodResolvers = new Map();
      parameterResolvers.set(target.constructor, methodResolvers);
    }
    const paramMetadata = methodResolvers.get(targetKey) || [];
    paramMetadata.push({
      index: parameterIndex,
      key: targetKey,
      resolver: (request: any) => request[paramName]
    });
    methodResolvers.set(targetKey, paramMetadata);
  };
}

// Uma função hipotética do framework para invocar um método com parâmetros resolvidos
function executeWithParams(instance: any, methodName: string, request: any) {
  const classResolvers = parameterResolvers.get(instance.constructor);
  if (!classResolvers) {
    return (instance[methodName] as Function).apply(instance, []);
  }
  const methodParamMetadata = classResolvers.get(methodName);
  if (!methodParamMetadata) {
    return (instance[methodName] as Function).apply(instance, []);
  }

  const args: any[] = Array(methodParamMetadata.length);
  for (const meta of methodParamMetadata) {
    args[meta.index] = meta.resolver(request);
  }
  return (instance[methodName] as Function).apply(instance, args);
}

class UserController {
  getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
    console.log(`Buscando usuário com ID: ${userId}, Token: ${authToken || "N/A"}`);
    return { id: userId, name: "Jane Doe" };
  }

  deleteUser(@RequestParam("id") userId: string) {
    console.log(`Excluindo usuário com ID: ${userId}`);
    return { status: "deleted", id: userId };
  }
}

const userController = new UserController();

// Simula uma requisição recebida
const mockRequest = {
  id: "user123",
  token: "abc-123",
  someOtherProp: "xyz"
};

console.log("\n--- Executando getUser ---");
executeWithParams(userController, "getUser", mockRequest);

console.log("\n--- Executando deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });

Este exemplo mostra como os decoradores de parâmetro podem coletar informações sobre os parâmetros de método necessários. Um framework pode então usar esses metadados coletados para resolver e injetar automaticamente valores apropriados quando o método é chamado, simplificando significativamente a lógica do controlador ou do serviço.

Composição e Ordem de Execução de Decoradores

Decoradores podem ser aplicados em várias combinações, e entender sua ordem de execução é crucial para prever o comportamento e evitar problemas inesperados.

Múltiplos Decoradores em um Único Alvo

Quando múltiplos decoradores são aplicados a uma única declaração (por exemplo, uma classe, método ou propriedade), eles são executados em uma ordem específica: de baixo para cima, ou da direita para a esquerda, para sua avaliação. No entanto, seus resultados são aplicados na ordem oposta.

@DecoradorA
@DecoradorB
class MinhaClasse {
  // ...
}

Aqui, DecoradorB será avaliado primeiro, depois DecoradorA. Se eles modificarem a classe (por exemplo, retornando um novo construtor), a modificação de DecoradorA envolverá ou aplicará sobre a modificação de DecoradorB.

Exemplo: Encadeamento de Decoradores de Método

Considere dois decoradores de método: LogCall e Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG] Chamando ${String(propertyKey)} com args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] Método ${String(propertyKey)} retornou:`, result);
    return result;
  };
  return descriptor;
}

function Authorization(roles: string[]) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const currentUserRoles = ["admin"]; // Simula a busca de roles do usuário atual
      const authorized = roles.some(role => currentUserRoles.includes(role));
      if (!authorized) {
        console.warn(`[AUTH] Acesso negado para ${String(propertyKey)}. Roles necessários: ${roles.join(", ")}`);
        throw new Error("Acesso não autorizado");
      }
      console.log(`[AUTH] Acesso concedido para ${String(propertyKey)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class SecureService {
  @LogCall
  @Authorization(["admin"])
  deleteSensitiveData(id: string) {
    console.log(`Excluindo dados sensíveis para ID: ${id}`);
    return `Dados ID ${id} excluídos.`;
  }

  @Authorization(["user"])
  @LogCall // Ordem alterada aqui
  fetchPublicData(query: string) {
    console.log(`Buscando dados públicos com query: ${query}`);
    return `Dados públicos para query: ${query}`; 
  }
}

const service = new SecureService();

try {
  console.log("\n--- Chamando deleteSensitiveData (Usuário Admin) ---");
  service.deleteSensitiveData("record123");
} catch (error: any) {
  console.error(error.message);
}

try {
  console.log("\n--- Chamando fetchPublicData (Usuário Não-Admin) ---");
  // Simula um usuário não-admin tentando acessar fetchPublicData, que requer o role 'user'
  const mockUserRoles = ["guest"]; // Isso falhará na autorização
  // Para tornar isso dinâmico, você precisaria de um sistema de DI ou contexto estático para os roles do usuário atual.
  // Para simplificar, assumimos que o decorador Authorization tem acesso ao contexto do usuário atual.
  // Vamos ajustar o decorador Authorization para sempre assumir 'admin' para fins de demonstração, 
  // então a primeira chamada terá sucesso e a segunda falhará para mostrar caminhos diferentes.
  
  // Re-executar com role de usuário para fetchPublicData ter sucesso.
  // Imagine que currentUserRoles em Authorization se torna: ['user']
  // Para este exemplo, vamos mantê-lo simples e mostrar o efeito da ordem.
  service.fetchPublicData("search term"); // Isso executará Auth -> Log
} catch (error: any) {
  console.error(error.message);
}

/* Saída esperada para deleteSensitiveData:
[AUTH] Acesso concedido para deleteSensitiveData
[LOG] Chamando deleteSensitiveData com args: [ 'record123' ]
Excluindo dados sensíveis para ID: record123
[LOG] Método deleteSensitiveData retornou: Dados ID record123 excluídos.
*/

/* Saída esperada para fetchPublicData (se o usuário tiver o role 'user'):
[LOG] Chamando fetchPublicData com args: [ 'search term' ]
[AUTH] Acesso concedido para fetchPublicData
Buscando dados públicos com query: search term
[LOG] Método fetchPublicData retornou: Dados públicos para query: search term
*/

Note a ordem: para deleteSensitiveData, Authorization (inferior) é executado primeiro, depois LogCall (superior) o envolve. A lógica interna de Authorization é executada primeiro. Para fetchPublicData, LogCall (inferior) é executado primeiro, depois Authorization (superior) o envolve. Isso significa que o aspecto LogCall estará fora do aspecto Authorization. Essa diferença é crítica para preocupações transversais como logging ou tratamento de erros, onde a ordem de execução pode impactar significativamente o comportamento.

Ordem de Execução para Diferentes Alvos

Quando uma classe, seus membros e parâmetros possuem decoradores, a ordem de execução é bem definida:

  1. Decoradores de Parâmetro são aplicados primeiro, para cada parâmetro, começando do último parâmetro para o primeiro.
  2. Em seguida, Decoradores de Método, Acessador ou Propriedade são aplicados para cada membro.
  3. Finalmente, Decoradores de Classe são aplicados à própria classe.

Dentro de cada categoria, múltiplos decoradores no mesmo alvo são aplicados de baixo para cima (ou da direita para a esquerda).

Exemplo: Ordem de Execução Completa

function log(message: string) {
  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
    if (typeof descriptorOrIndex === 'number') {
      console.log(`Decorador de Parâmetro: ${message} no parâmetro #${descriptorOrIndex} de ${String(propertyKey || "constructor")}`);
    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
        console.log(`Decorador de Método/Acessador: ${message} em ${String(propertyKey)}`);
      } else {
        console.log(`Decorador de Propriedade: ${message} em ${String(propertyKey)}`);
      }
    } else {
      console.log(`Decorador de Classe: ${message} em ${target.name}`);
    }
    return descriptorOrIndex; // Retorna o descritor para método/acessador, undefined para outros
  };
}

@log("Decorador de Classe D")
@log("Decorador de Classe C")
class MyDecoratedClass {
  @log("Propriedade Estática A")
  static staticProp: string = "";

  @log("Propriedade de Instância B")
  instanceProp: number = 0;

  @log("Método D")
  @log("Método C")
  myMethod(
    @log("Parâmetro Z") paramZ: string,
    @log("Parâmetro Y") paramY: number
  ) {
    console.log("Método myMethod executado.");
  }

  @log("Getter/Setter F")
  get myAccessor() {
    return "";
  }

  set myAccessor(value: string) {
    //...
  }

  constructor() {
    console.log("Construtor executado.");
  }
}

new MyDecoratedClass();
// Chama o método para acionar o decorador do método
new MyDecoratedClass().myMethod("hello", 123);

/* Ordem de Saída Prevista (aproximada, dependendo da versão específica do TypeScript e compilação):
Decorador de Parâmetro: Parâmetro Y no parâmetro #1 de myMethod
Decorador de Parâmetro: Parâmetro Z no parâmetro #0 de myMethod
Decorador de Propriedade: Propriedade Estática A em staticProp
Decorador de Propriedade: Propriedade de Instância B em instanceProp
Decorador de Método/Acessador: Getter/Setter F em myAccessor
Decorador de Método/Acessador: Método C em myMethod
Decorador de Método/Acessador: Método D em myMethod
Decorador de Classe: Decorador de Classe C em MyDecoratedClass
Decorador de Classe: Decorador de Classe D em MyDecoratedClass
Construtor executado.
Método myMethod executado.
*/

O tempo exato do log pode variar ligeiramente com base em quando um construtor ou método é invocado, mas a ordem em que as próprias funções decoradoras são executadas (e, portanto, seus efeitos colaterais ou valores de retorno aplicados) segue as regras acima.

Aplicações Práticas e Padrões de Design com Decoradores

Decoradores, especialmente em conjunto com o polyfill reflect-metadata, abrem um novo reino de programação baseada em metadados. Isso permite padrões de design poderosos que abstraem boilerplate e preocupações transversais.

1. Injeção de Dependência (DI)

Um dos usos mais proeminentes de decoradores é em frameworks de Injeção de Dependência (como @Injectable(), @Component(), etc. do Angular, ou o uso extensivo de DI no NestJS). Decoradores permitem declarar dependências diretamente em construtores ou propriedades, permitindo que o framework instancie e forneça automaticamente os serviços corretos.

Exemplo: Injeção Simplificada de Serviço

import "reflect-metadata"; // Essencial para emitDecoratorMetadata

const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");

function Injectable() {
  return function (target: Function) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
  };
}

function Inject(token: any) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
    existingInjections[parameterIndex] = token;
    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
  };
}

class Container {
  private static instances = new Map<any, any>();

  static resolve<T>(target: { new (...args: any[]): T }): T {
    if (Container.instances.has(target)) {
      return Container.instances.get(target);
    }

    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
    if (!isInjectable) {
      throw new Error(`Classe ${target.name} não está marcada como @Injectable.`);
    }

    // Obtém os tipos de parâmetros do construtor (requer emitDecoratorMetadata)
    const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];

    const dependencies = paramTypes.map((paramType, index) => {
      // Usa o token @Inject explícito se fornecido, senão infere o tipo
      const token = explicitInjections[index] || paramType;
      if (token === undefined) {
        throw new Error(`Não é possível resolver o parâmetro no índice ${index} para ${target.name}. Pode ser uma dependência circular ou um tipo primitivo sem @Inject explícito.`);
      }
      return Container.resolve(token);
    });

    const instance = new target(...dependencies);
    Container.instances.set(target, instance);
    return instance;
  }
}

// Define os serviços
@Injectable()
class DatabaseService {
  connect() {
    console.log("Conectando ao banco de dados...");
    return "Conexão DB";
  }
}

@Injectable()
class AuthService {
  private db: DatabaseService;

  constructor(db: DatabaseService) {
    this.db = db;
  }

  login() {
    console.log(`AuthService: Autenticando usando ${this.db.connect()}`);
    return "Usuário logado";
  }
}

@Injectable()
class UserService {
  private authService: AuthService;
  private dbService: DatabaseService; // Exemplo de injeção via propriedade usando um decorador customizado ou recurso do framework

  constructor(@Inject(AuthService) authService: AuthService,
              @Inject(DatabaseService) dbService: DatabaseService) {
    this.authService = authService;
    this.dbService = dbService;
  }

  getUserProfile() {
    this.authService.login();
    this.dbService.connect();
    console.log("UserService: Buscando perfil do usuário...");
    return { id: 1, name: "Usuário Global" };
  }
}

// Resolve o serviço principal
console.log("--- Resolvendo UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());

console.log("\n--- Resolvendo AuthService (deve ser cacheado) ---");
const authService = Container.resolve(AuthService);
authService.login();

Este elaborado exemplo demonstra como os decoradores @Injectable e @Inject, combinados com reflect-metadata, permitem que um Container customizado resolva e forneça dependências automaticamente. Os metadados design:paramtypes emitidos automaticamente pelo TypeScript (quando emitDecoratorMetadata está habilitado) são cruciais aqui.

2. Programação Orientada a Aspectos (AOP)

AOP foca na modularização de preocupações transversais (por exemplo, logging, segurança, transações) que atravessam múltiplas classes e módulos. Decoradores são um excelente encaixe para implementar conceitos AOP em TypeScript.

Exemplo: Logging com Decorador de Método

Revisando o decorador LogCall, ele é um exemplo perfeito de AOP. Ele adiciona comportamento de logging a qualquer método sem modificar o código original do método. Isso separa o "o que fazer" (lógica de negócios) do "como fazer" (logging, monitoramento de desempenho, etc.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`[LOG AOP] Entrando no método: ${String(propertyKey)} com args:`, args);
    try {
      const result = originalMethod.apply(this, args);
      console.log(`[LOG AOP] Saindo do método: ${String(propertyKey)} com resultado:`, result);
      return result;
    } catch (error: any) {
      console.error(`[LOG AOP] Erro no método ${String(propertyKey)}:`, error.message);
      throw error;
    }
  };
  return descriptor;
}

class PaymentProcessor {
  @LogMethod
  processPayment(amount: number, currency: string) {
    if (amount <= 0) {
      throw new Error("O valor do pagamento deve ser positivo.");
    }
    console.log(`Processando pagamento de ${amount} ${currency}...`);
    return `Pagamento de ${amount} ${currency} processado com sucesso.`;
  }

  @LogMethod
  refundPayment(transactionId: string) {
    console.log(`Reembolsando pagamento para ID de transação: ${transactionId}...`);
    return `Reembolso iniciado para ${transactionId}.`;
  }
}

const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
  processor.processPayment(-50, "EUR");
} catch (error: any) {
  console.error("Erro capturado:", error.message);
}

Essa abordagem mantém a classe PaymentProcessor focada puramente na lógica de pagamento, enquanto o decorador LogMethod lida com a preocupação transversal de logging.

3. Validação e Transformação

Decoradores são incrivelmente úteis para definir regras de validação diretamente em propriedades ou para transformar dados durante a serialização/desserialização.

Exemplo: Validação de Dados com Decoradores de Propriedade

O exemplo @Required anterior já demonstrou isso. Aqui está outro exemplo com validação de intervalo numérico.

interface FieldValidationRule {
  property: string | symbol;
  validator: (value: any) => boolean;
  message: string;
}

const fieldValidationRules = new Map<Function, FieldValidationRule[]>();

function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
  const rules = fieldValidationRules.get(target.constructor) || [];
  rules.push({ property: propertyKey, validator, message });
  fieldValidationRules.set(target.constructor, rules);
}

function IsPositive(target: Object, propertyKey: string | symbol) {
  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} deve ser um número positivo.`);
}

function MaxLength(maxLength: number) {
  return function (target: Object, propertyKey: string | symbol) {
    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} deve ter no máximo ${maxLength} caracteres.`);
  };
}

class Product {
  @MaxLength(50)
  name: string;

  @IsPositive
  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }

  static validate(instance: any): string[] {
    const errors: string[] = [];
    const rules = fieldValidationRules.get(instance.constructor) || [];
    for (const rule of rules) {
      if (!rule.validator(instance[rule.property])) {
        errors.push(rule.message);
      }
    }
    return errors;
  }
}

const product1 = new Product("Laptop", 1200);
console.log("Erros do Produto 1:", Product.validate(product1)); // []

const product2 = new Product("Nome de produto muito longo que excede o limite de cinquenta caracteres para fins de teste", 50);
console.log("Erros do Produto 2:", Product.validate(product2)); // ["name deve ter no máximo 50 caracteres."]

const product3 = new Product("Livro", -10);
console.log("Erros do Produto 3:", Product.validate(product3)); // ["price deve ser um número positivo."]

Esta configuração permite definir declarativamente regras de validação nas propriedades do seu modelo, tornando seus modelos de dados autodescritivos em termos de suas restrições.

Melhores Práticas e Considerações

Embora os decoradores sejam poderosos, eles devem ser usados com critério. O uso indevido deles pode levar a um código mais difícil de depurar ou entender.

Quando Usar Decoradores (e Quando Não)

Implicações de Desempenho

Decoradores são executados em tempo de compilação (ou tempo de definição em tempo de execução JavaScript se transpilados). A transformação ou coleta de metadados ocorre quando a classe/método é definida, não em cada chamada. Portanto, o impacto de desempenho em tempo de execução de *aplicar* decoradores é mínimo. No entanto, a *lógica dentro* dos seus decoradores pode ter um impacto no desempenho, especialmente se eles executarem operações caras em cada chamada de método (por exemplo, cálculos complexos dentro de um decorador de método).

Manutenibilidade e Legibilidade

Decoradores, quando usados corretamente, podem melhorar significativamente a legibilidade ao remover código boilerplate da lógica principal. No entanto, se eles executarem transformações complexas e ocultas, a depuração pode se tornar desafiadora. Certifique-se de que seus decoradores sejam bem documentados e que seu comportamento seja previsível.

Status Experimental e Futuro dos Decoradores

É importante reiterar que os decoradores TypeScript são baseados em uma proposta TC39 de Estágio 3. Isso significa que a especificação é em grande parte estável, mas ainda pode sofrer pequenas alterações antes de se tornar parte do padrão oficial ECMAScript. Frameworks como o Angular os abraçaram, apostando em sua eventual padronização. Isso implica um certo nível de risco, embora dada sua adoção generalizada, mudanças drásticas sejam improváveis.

A proposta TC39 evoluiu. A implementação atual do TypeScript é baseada em uma versão mais antiga da proposta. Existe uma distinção entre "Decoradores Legados" e "Decoradores Padrão". Quando o padrão oficial chegar, o TypeScript provavelmente atualizará sua implementação. Para a maioria dos desenvolvedores que usam frameworks, essa transição será gerenciada pelo próprio framework. Para autores de bibliotecas, entender as sutis diferenças entre decoradores legados e futuros padrões pode se tornar necessário.

A Opção do Compilador emitDecoratorMetadata

Esta opção, quando definida como true em tsconfig.json, instrui o compilador TypeScript a emitir certos metadados de tipo em tempo de design no JavaScript compilado. Esses metadados incluem o tipo dos parâmetros do construtor (design:paramtypes), o tipo de retorno de métodos (design:returntype) e o tipo de propriedades (design:type).

Esses metadados emitidos não fazem parte do runtime padrão do JavaScript. Eles são tipicamente consumidos pelo polyfill reflect-metadata, que então os torna acessíveis através das funções Reflect.getMetadata(). Isso é absolutamente crítico para padrões avançados como Injeção de Dependência, onde um contêiner precisa conhecer os tipos de dependências que uma classe requer sem configuração explícita.

Padrões Avançados com Decoradores

Decoradores podem ser combinados e estendidos para construir padrões ainda mais sofisticados.

1. Decorando Decoradores (Decoradores de Ordem Superior)

Você pode criar decoradores que modificam ou compõem outros decoradores. Isso é menos comum, mas demonstra a natureza funcional dos decoradores.

// Um decorador que garante que um método seja logado e também exija roles de admin
function AdminAndLoggedMethod() {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // Aplica Authorization primeiro (interno)
    Authorization(["admin"])(target, propertyKey, descriptor);
    // Em seguida, aplica LogCall (externo)
    LogCall(target, propertyKey, descriptor);

    return descriptor; // Retorna o descritor modificado
  };
}

class AdminPanel {
  @AdminAndLoggedMethod()
  deleteUserAccount(userId: string) {
    console.log(`Excluindo conta de usuário: ${userId}`);
    return `Usuário ${userId} excluído.`;
  }
}

const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Saída Esperada (assumindo role de admin):
[AUTH] Acesso concedido para deleteUserAccount
[LOG] Chamando deleteUserAccount com args: [ 'user007' ]
Excluindo conta de usuário: user007
[LOG] Método deleteUserAccount retornou: Usuário user007 excluído.
*/

Aqui, AdminAndLoggedMethod é uma fábrica que retorna um decorador, e dentro desse decorador, ele aplica outros dois decoradores. Este padrão pode encapsular composições complexas de decoradores.

2. Usando Decoradores para Mixins

Embora o TypeScript ofereça outras formas de implementar mixins, decoradores podem ser usados para injetar capacidades em classes de forma declarativa.

function ApplyMixins(constructors: Function[]) {
  return function (derivedConstructor: Function) {
    constructors.forEach(baseConstructor => {
      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
        Object.defineProperty(
          derivedConstructor.prototype,
          name,
          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
        );
      });
    });
  };
}

class Disposable {
  isDisposed: boolean = false;
  dispose() {
    this.isDisposed = true;
    console.log("Objeto descartado.");
  }
}

class Loggable {
  log(message: string) {
    console.log(`[Loggable] ${message}`);
  }
}

@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
  // Essas propriedades/métodos são injetados pelo decorador
  isDisposed!: boolean;
  dispose!: () => void;
  log!: (message: string) => void;

  constructor(public name: string) {
    this.log(`Recurso ${this.name} criado.`);
  }

  cleanUp() {
    this.dispose();
    this.log(`Recurso ${this.name} limpo.`);
  }
}

const resource = new MyResource("NetworkConnection");
console.log(`Está descartado: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Está descartado: ${resource.isDisposed}`);

Este decorador @ApplyMixins copia dinamicamente métodos e propriedades de construtores base para o protótipo da classe derivada, efetivamente "misturando" funcionalidades.

Conclusão: Capacitando o Desenvolvimento Moderno com TypeScript

Os decoradores TypeScript são um recurso poderoso e expressivo que permite um novo paradigma de programação baseada em metadados e orientada a aspectos. Eles permitem que os desenvolvedores aprimorem, modifiquem e adicionem comportamentos declarativos a classes, métodos, propriedades, acessadores e parâmetros sem alterar sua lógica central. Essa separação de preocupações leva a um código mais limpo, mais mantenível e altamente reutilizável.

Desde simplificar a injeção de dependência e implementar sistemas robustos de validação até adicionar preocupações transversais como logging e monitoramento de desempenho, os decoradores fornecem uma solução elegante para muitos desafios comuns de desenvolvimento.

Ao dominar os decoradores TypeScript, você ganha uma ferramenta significativa em seu arsenal, permitindo que você construa aplicações mais robustas, escaláveis e inteligentes. Adote-os de forma responsável, entenda sua mecânica e desbloqueie um novo nível de poder declarativo em seus projetos TypeScript.