<h2>O que são Decorators em TypeScript</h2>
<p>Decorators são uma feature experimental do TypeScript que permite você modificar e anotar classes, métodos, propriedades e parâmetros no tempo de definição. Pense neles como funções especiais que "envolvem" ou "decoram" o elemento alvo, alterando seu comportamento ou adicionando metadados sem modificar o código original. Essa capacidade é poderosa porque separa responsabilidades: o seu código de negócio permanece limpo enquanto comportamentos transversais (como validação, logging ou autenticação) ficam encapsulados no decorator.</p>
<p>A origem dos decorators vem do padrão Decorator clássico, mas TypeScript os implementa de forma mais declarativa, inspirado em linguagens como Java e Python. Para usar decorators, você precisa habilitar a flag <code>experimentalDecorators</code> no arquivo <code>tsconfig.json</code>. Essa feature ainda está em proposta TC39, mas é amplamente utilizada em frameworks como NestJS, Angular e Inversify, provando sua utilidade prática.</p>
<pre><code class="language-json">{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}</code></pre>
<h2>Class Decorators: Decorando Toda uma Classe</h2>
<p>Um class decorator é uma função que recebe o construtor da classe como argumento e pode retornar um novo construtor que estende o original. Ela é executada quando a classe é definida, não quando uma instância é criada. Isso permite você adicionar propriedades, métodos ou alterar o comportamento de construção sem tocar na classe original.</p>
<p>O padrão é simples: o decorator recebe a classe (função construtora) e retorna uma classe modificada. Você pode usar isso para adicionar funcionalidades genéricas como timestamp de criação, registrar instâncias, ou implementar padrões como Singleton.</p>
<pre><code class="language-typescript">function Loggable(constructor: Function) {
console.log(Classe ${constructor.name} foi definida);
const original = constructor;
const newConstructor: any = function (...args: any[]) {
console.log(Nova instância de ${original.name} criada);
original.apply(this, args);
};
newConstructor.prototype = original.prototype;
return newConstructor;
}
@Loggable
class Usuario {
constructor(public nome: string) {}
}
const user = new Usuario('João');
// Output:
// Classe Usuario foi definida
// Nova instância de Usuario criada</code></pre>
<p>Um exemplo mais prático: criar um decorator que adiciona um método estático para contar instâncias criadas.</p>
<pre><code class="language-typescript">function Countable<T extends { new(...args: any[]): {} }>(constructor: T) {
let count = 0;
const newConstructor: any = function (...args: any[]) {
count++;
constructor.apply(this, args);
};
newConstructor.prototype = constructor.prototype;
newConstructor.getCount = () => count;
return newConstructor;
}
@Countable
class Produto {
constructor(public nome: string) {}
}
new Produto('Notebook');
new Produto('Mouse');
new Produto('Teclado');
console.log((Produto as any).getCount()); // 3</code></pre>
<h2>Method Decorators: Aprimorando Métodos</h2>
<p>Method decorators recebem três parâmetros: o objeto alvo (o prototype da classe), o nome do método (string ou symbol), e o descritor de propriedade. O descritor contém a função original, e você pode retornar um novo descritor que envolve a função com lógica adicional. Isso é especialmente útil para logging, timing, cache, validação e tratamento de erros.</p>
<p>O caso de uso mais comum é medir o tempo de execução de um método ou registrar chamadas de métodos. Aqui está um exemplo que faz exatamente isso:</p>
<pre><code class="language-typescript">function Timing(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const inicio = performance.now();
const result = originalMethod.apply(this, args);
const fim = performance.now();
console.log(${propertyKey} levou ${fim - inicio}ms para executar);
return result;
};
return descriptor;
}
class Calculadora {
@Timing
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const calc = new Calculadora();
calc.fibonacci(10);
// Output: fibonacci levou 0.1234ms para executar</code></pre>
<p>Outro exemplo prático: um decorator que valida argumentos antes de executar o método.</p>
<pre><code class="language-typescript">function ValidatePositive(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (valor: number) {
if (valor < 0) {
throw new Error(${propertyKey} recebeu valor negativo: ${valor});
}
return originalMethod.apply(this, valor);
};
return descriptor;
}
class ContaBancaria {
saldo: number = 1000;
@ValidatePositive
sacar(valor: number): number {
this.saldo -= valor;
return this.saldo;
}
}
const conta = new ContaBancaria();
conta.sacar(100); // ok
conta.sacar(-50); // Error: sacar recebeu valor negativo: -50</code></pre>
<h2>Property Decorators: Marcando e Validando Propriedades</h2>
<p>Property decorators operam sobre propriedades de classe, recebendo o objeto alvo e o nome da propriedade. Diferente de method decorators, o descriptor de propriedade é mais limitado, mas você pode usá-los para adicionar metadados ou implementar getters/setters que validam valores. Essa feature é particularmente útil em frameworks de validação e ORM.</p>
<p>Property decorators não interceptam atribuições diretamente, mas você pode combinar com getters e setters para criar comportamentos sofisticados. Um caso prático é marcar propriedades para serialização ou criar validators personalizados.</p>
<pre><code class="language-typescript">function Required(target: any, propertyKey: string) {
let value: any;
const getter = function() {
return value;
};
const setter = function(newVal: any) {
if (!newVal || (typeof newVal === 'string' && newVal.trim() === '')) {
throw new Error(${propertyKey} é obrigatório);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Pessoa {
@Required
nome: string = '';
@Required
email: string = '';
}
const pessoa = new Pessoa();
pessoa.nome = ''; // Error: nome é obrigatório</code></pre>
<p>Um exemplo mais realista: criar um decorator que armazena metadados sobre quais propriedades devem ser serializadas para JSON.</p>
<pre><code class="language-typescript">const metadataKey = Symbol('serializable');
function Serializable(target: any, propertyKey: string) {
const existingMetadata = Reflect.getOwnMetadata(metadataKey, target) || [];
existingMetadata.push(propertyKey);
Reflect.defineMetadata(metadataKey, existingMetadata, target);
}
class Usuário {
@Serializable
id: number = 1;
@Serializable
email: string = 'user@example.com';
senha: string = 'secreto'; // não será serializado
}
function toJSON(obj: any) {
const metadados = Reflect.getOwnMetadata(metadataKey, obj) || [];
const resultado: any = {};
metadados.forEach((prop: string) => {
resultado[prop] = obj[prop];
});
return resultado;
}
const usuario = new Usuário();
console.log(toJSON(usuario));
// Output: { id: 1, email: 'user@example.com' }</code></pre>
<h2>Parameter Decorators: Anotando Parâmetros de Função</h2>
<p>Parameter decorators recebem três argumentos: o objeto alvo, a chave do método (string), e o índice do parâmetro (number). Eles são úteis para adicionar metadados sobre parâmetros que serão consumidos por method decorators ou em frameworks de injeção de dependência. Na verdade, parameter decorators raramente funcionam sozinhos — precisam trabalhar em conjunto com method decorators ou class decorators.</p>
<p>O caso de uso mais importante é em frameworks como NestJS, onde decorators de parâmetro marcam qual parâmetro vem do body da requisição, da URL, dos headers, etc. Vou mostrar um exemplo simplificado que funciona de forma autônoma:</p>
<pre><code class="language-typescript">function LogParameter(target: any, propertyKey: string | symbol | undefined, parameterIndex: number) { const existingLogParameters: number[] = Reflect.getOwnMetadata('log:parameters', target, propertyKey) || [];
existingLogParameters.push(parameterIndex);
Reflect.defineMetadata('log:parameters', existingLogParameters, target, propertyKey);
}
class Calculadora {
somar(
@LogParameter a: number,
@LogParameter b: number
): number {
return a + b;
}
}
const calc = new Calculadora();
const metadados = Reflect.getOwnMetadata('log:parameters', calc, 'somar');
console.log(metadados); // [0, 1]</code></pre>
<p>Um exemplo mais prático: criar um decorator que valida o tipo do parâmetro em tempo de execução.</p>
<pre><code class="language-typescript">function ValidateType(expectedType: any) {
return function(target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
const metadataKey = validate:${String(propertyKey)};
const existingMetadata = Reflect.getOwnMetadata(metadataKey, target) || {};
existingMetadata[parameterIndex] = expectedType;
Reflect.defineMetadata(metadataKey, existingMetadata, target);
};
}
function ValidateParameters(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const metadataKey = validate:${propertyKey};
const typesToValidate = Reflect.getOwnMetadata(metadataKey, target) || {};
descriptor.value = function (...args: any[]) {
Object.keys(typesToValidate).forEach((index: string) => {
const expectedType = typesToValidate[index];
const argValue = args[parseInt(index)];
if (typeof argValue !== typeof expectedType().constructor.name.toLowerCase()) {
throw new TypeError(Parâmetro ${index} deve ser do tipo ${expectedType.name});
}
});
return originalMethod.apply(this, args);
};
return descriptor;
}</code></pre>
<h2>Caso Prático Integrado: Sistema de Autorização</h2>
<p>Para consolidar o conhecimento, vou criar um exemplo real que combina class, method e parameter decorators em um cenário de autenticação e autorização.</p>
<pre><code class="language-typescript">interface Usuario {
id: number;
nome: string;
roles: string[];
}
let usuarioAtual: Usuario | null = null;
function SetUser(usuario: Usuario) {
usuarioAtual = usuario;
}
function RequireRole(...roles: string[]) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!usuarioAtual) {
throw new Error('Usuário não autenticado');
}
const hasRole = roles.some(role => usuarioAtual!.roles.includes(role));
if (!hasRole) {
throw new Error(Acesso negado. Roles necessárias: ${roles.join(', ')});
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
function Audit(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log([AUDIT] ${usuarioAtual?.nome || 'Anônimo'} executou ${propertyKey});
console.log([AUDIT] Argumentos:, args);
const result = originalMethod.apply(this, args);
console.log([AUDIT] Resultado:, result);
return result;
};
return descriptor;
}
class ServicoRelatorios {
@RequireRole('admin', 'analista')
@Audit
gerarRelatorio(tipo: string, periodo: string): string {
return Relatório de ${tipo} para ${periodo};
}
@RequireRole('admin')
@Audit
deletarRelatorio(id: number): boolean {
return true;
}
}
// Uso
const relatorios = new ServicoRelatorios();
// Sem usuário
try {
relatorios.gerarRelatorio('vendas', '2024');
} catch (e) {
console.log(Erro: ${(e as Error).message});
}
// Com usuário sem permissão
SetUser({ id: 1, nome: 'João', roles: ['usuario'] });
try {
relatorios.gerarRelatorio('vendas', '2024');
} catch (e) {
console.log(Erro: ${(e as Error).message});
}
// Com usuário com permissão
SetUser({ id: 2, nome: 'Maria', roles: ['admin'] });
console.log(relatorios.gerarRelatorio('vendas', '2024'));
// Output:
// [AUDIT] Maria executou gerarRelatorio
// [AUDIT] Argumentos: [ 'vendas', '2024' ]
// [AUDIT] Resultado: Relatório de vendas para 2024</code></pre>
<h2>Conclusão</h2>
<p>Você aprendeu que <strong>decorators são funções que modificam comportamento de classes, métodos, propriedades e parâmetros de forma declarativa</strong>, permitindo separar lógica transversal (logging, autenticação, validação) do código de negócio. Cada tipo de decorator tem seu propósito específico: class decorators para transformar a classe inteira, method decorators para envolver chamadas de método, property decorators para validar atribuições, e parameter decorators para anotar dados de parâmetros.</p>
<p>O segundo ponto essencial é que <strong>decorators funcionam melhor quando combinados com a Reflect API do TypeScript</strong>, especialmente para armazenar metadados que serão consumidos por outros decorators. Essa combinação é o fundamento de frameworks profissionais como NestJS, que usam metadados para criar IoC containers e injeção de dependência.</p>
<p>Por fim, lembre-se que <strong>decorators ainda são uma feature experimental</strong>, mas amplamente adotada em produção. Habilitá-los no <code>tsconfig.json</code> é necessário, e entender quando usá-los versus criar classes wrapper é uma questão de design. Use decorators quando precisar aplicar comportamento genérico a múltiplos elementos; use padrões clássicos quando a lógica é específica do domínio.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/decorators.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Decorators</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect" target="_blank" rel="noopener noreferrer">MDN Web Docs - Decorators (Proposal)</a></li>
<li><a href="https://docs.nestjs.com/custom-decorators" target="_blank" rel="noopener noreferrer">NestJS Documentation - Custom Decorators</a></li>
<li><a href="https://basarat.gitbook.io/typescript/main-1/decorators" target="_blank" rel="noopener noreferrer">TypeScript Deep Dive - Decorators</a></li>
<li><a href="https://github.com/tc39/proposal-decorators" target="_blank" rel="noopener noreferrer">ECMAScript Proposal - Decorators (TC39)</a></li>
</ul>
<p><!-- FIM --></p>