Os decorators do TypeScript permitem que você anexe comportamentos a classes, métodos, acessores, propriedades e parâmetros sem modificar o código original diretamente. Eles são mais visíveis no Angular, mas o padrão é útil em qualquer lugar onde você queira manter preocupações transversais — como logging, validação ou metadados — fora da lógica de negócio central. Este post aborda como os decorators funcionam, os cinco tipos suportados pelo TypeScript e como criar o seu próprio.

Entendendo os Fundamentos dos Decorators do TypeScript

Um decorator é uma função. Quando você coloca @Something acima de uma classe ou método, o TypeScript chama essa função com metadados sobre o alvo: o construtor, o descriptor do método, o nome da propriedade e assim por diante. O decorator pode encapsular comportamentos, anexar novas propriedades ou registrar metadados para uso posterior.

Por Que os Decorators Importam

Os decorators resolvem um problema específico: preocupações transversais se acumulam nos métodos ao longo do tempo. Logging, métricas, validação e verificações de permissão acabam espalhados dentro de funções que deveriam se preocupar com apenas uma coisa. Os decorators permitem extrair essa lógica e reutilizá-la em toda a base de código com uma única anotação.

Depois de escrever um decorator de logging, você pode aplicá-lo a centenas de métodos sem tocar em nenhum deles. Quando o comportamento de logging precisar mudar, você altera em um único lugar. Esse é o valor real — não a sintaxe.

O Angular usa decorators extensivamente para @Component, @Injectable, @Input e marcadores similares. Entender o que acontece por baixo dos panos ajuda quando você precisa depurar, construir extensões de biblioteca customizadas ou escrever código agnóstico ao framework.

Tipos de Decorators no TypeScript

O TypeScript suporta cinco tipos de decorators, cada um direcionado a um construto de linguagem diferente.

Decorators de classe recebem um construtor como argumento. Podem encapsular o construtor, anexar novas propriedades ao prototype ou substituir a classe inteiramente. O uso mais comum é marcar classes com papéis ou executar lógica de inicialização.

Decorators de método recebem o prototype da classe, o nome do método e o property descriptor. Você substitui descriptor.value por uma função wrapper. É o tipo mais utilizado, ideal para logging, caching e controle de acesso.

Decorators de acessor funcionam da mesma forma que os decorators de método, mas visam getters e setters. Úteis para validação ou transformação no acesso a propriedades, embora menos necessários do que os decorators de método.

Decorators de propriedade recebem o alvo e o nome da propriedade. Não podem modificar inicializadores de propriedade diretamente, mas podem armazenar metadados via reflect-metadata. ORMs usam esse padrão extensivamente para mapear propriedades de classe a colunas de banco de dados.

Decorators de parâmetro anotam parâmetros de métodos com seu índice na assinatura. Frameworks de injeção de dependência usam isso para saber quais argumentos injetar automaticamente.

Pré-requisitos para Usar Decorators

Habilite duas opções do compilador no seu tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  },
  ...
}

experimentalDecorators habilita a sintaxe de decorators. emitDecoratorMetadata emite informações de tipo em tempo de execução, que a biblioteca reflect-metadata utiliza para cenários avançados de reflexão, como injeção de dependência.

Como Usar Decorators na Prática

Exemplo de Decorator de Classe

Aqui está um decorator de classe simples que adiciona uma propriedade de timestamp para rastrear os tempos de instanciação:

function Timestamped(constructor: Function) {
  constructor.prototype.createdAt = new Date();
}

@Timestamped
class ReportService {
  constructor() {
    // Some initialization code
  }

  generateReport() {
    console.log("Report generated");
  }
}

const service = new ReportService();
console.log(service["createdAt"]); // A Date object representing creation time

Timestamped modifica o prototype do construtor, então cada instância de ReportService recebe uma propriedade createdAt definida para o momento em que a classe foi decorada pela primeira vez. Note que isso é executado uma vez no momento da definição da classe, não a cada instanciação — um erro comum para quem está começando.

Exemplo de Decorator de Método

Os decorators de método permitem interceptar e ampliar chamadas de função. Aqui está um decorator de logging simples:

function LogMethod(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyName} with arguments:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Result of ${propertyName}:`, result);
    return result;
  };

  return descriptor;
}

class TaskService {
  @LogMethod
  runTask(taskId: string) {
    return `Task ${taskId} completed successfully!`;
  }
}

const taskService = new TaskService();
taskService.runTask("1234");

LogMethod substitui descriptor.value por um wrapper que registra logs antes e depois da chamada original. O método original é executado via apply(this, args) para preservar o binding correto de this.

Exemplo de Decorator de Propriedade

Os decorators de propriedade podem anexar metadados a propriedades específicas de uma classe. Por exemplo, imagine um ORM que precisa identificar quais propriedades mapeiam para colunas do banco de dados:

import "reflect-metadata";

const COLUMN_KEY = Symbol("column");

/**
 * Attach metadata to a property to mark it as a DB column.
 */
function Column(columnName?: string): PropertyDecorator {
  return (target, propertyKey) => {
    Reflect.defineMetadata(COLUMN_KEY, columnName || propertyKey, target, propertyKey);
  };
}

class User {
  @Column("user_id")
  id: number;

  @Column()
  username: string;

  @Column()
  email: string;
}

// Later you can retrieve metadata to build queries or define migrations
function getColumnMetadata(target: any, propertyKey: string) {
  return Reflect.getMetadata(COLUMN_KEY, target, propertyKey);
}

Cada propriedade decorada armazena metadados sobre o nome de sua coluna. A camada do ORM lê esses metadados em tempo de execução para construir queries, sem que nenhuma dessa lógica resida dentro da própria classe User.

Como Criar Seus Próprios Decorators

Construir um decorator customizado significa escrever uma função que corresponda à assinatura esperada para o tipo de alvo. O exemplo de validador abaixo combina um decorator de parâmetro e um decorator de método para garantir argumentos não vazios.

Exemplo Passo a Passo: Decorator de Validação Customizado

  1. Crie a função decorator:
import "reflect-metadata";

const VALIDATE_KEY = Symbol("validateParams");

function NotEmpty(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  let existingRequiredParameters: number[] =
    Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    VALIDATE_KEY,
    existingRequiredParameters,
    target,
    propertyKey
  );
}
  1. Crie uma função auxiliar para executar as validações antes de chamar o método original:
function Validate(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const requiredParameters: number[] =
      Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyName) || [];

    requiredParameters.forEach((index) => {
      if (args[index] === null || args[index] === undefined || args[index] === "") {
        throw new Error(`Validation Error: Parameter at index ${index} is empty`);
      }
    });

    return method.apply(this, args);
  };

  return descriptor;
}
  1. Aplique esses decorators em uma classe:
class MailService {
  @Validate
  sendEmail(@NotEmpty recipient: string, message: string) {
    console.log(`Sending email to ${recipient} with message: ${message}`);
  }
}

const mailService = new MailService();
mailService.sendEmail("john@example.com", "Hello John!"); // Works fine
mailService.sendEmail("", "This should fail validation"); // Throws Validation Error

@NotEmpty registra quais índices de parâmetros requerem validação. @Validate lê esses índices antes de invocar o método original e lança um erro se algum deles estiver vazio. A lógica de validação reside inteiramente nos decorators, não em sendEmail.

Decorators no Angular

O Angular usa decorators de classe para marcar componentes, serviços, diretivas e pipes. Quando você escreve @Component(...), o compilador do Angular lê os metadados desse decorator para entender como compilar e conectar a classe. @Injectable() informa ao sistema de injeção de dependência que a classe pode ser instanciada e injetada em outros lugares.

Você não precisa entender os internos para usar o Angular, mas saber como os decorators funcionam torna a depuração muito menos misteriosa quando metadados estão ausentes ou mal configurados.

Boas Práticas para Decorators

Mantenha cada decorator focado em uma única responsabilidade. Um decorator que faz logging, valida e transforma dados é mais difícil de depurar do que três separados. Quando decorators modificam prototypes ou descriptors, documente o que eles alteram para que leitores futuros não precisem fazer engenharia reversa. A biblioteca reflect-metadata é poderosa, mas pode se tornar difícil de raciocinar se usada em excesso. Se você estiver anexando metadados para um único caso de uso, considere se uma abordagem mais simples não funcionaria melhor.

Teste seus decorators explicitamente. Por aplicarem comportamento transversal, é fácil ignorá-los nos testes unitários. Escreva testes que verifiquem o comportamento do decorator isoladamente, não apenas o método que ele encapsula.

Avançando com Decorators do TypeScript

O padrão escala bem. Decorator factories permitem passar argumentos de configuração para um decorator. Empilhar múltiplos decorators em um único método é simples, embora a ordem de execução (de baixo para cima) cause confusão no início. Para projetos maiores, os decorators se combinam bem com padrões de injeção de dependência e middleware.

O principal ponto a internalizar é que um decorator é apenas uma função. Quando isso fica claro, o resto é mecânica.