Decorators no TypeScript: os antigos, os novos, e quando usar
Os decorators são aquela sintaxe com @ por cima de uma classe ou de um método. Quem já usou Angular, NestJS ou TypeORM viu-os às dezenas — e quem nunca usou nenhum desses provavelmente nunca precisou de escrever um.
Há uma confusão a arrumar primeiro: existem dois sistemas de decorators, incompatíveis entre si, e quase todos os exemplos que se encontram na internet são do antigo.
1. Os dois sistemas
Os antigos — ligam-se com
experimentalDecorators: truenotsconfig.json. Vêm de uma proposta que nunca chegou a norma, e é neles que assentam o Angular, o NestJS e o TypeORM.Os novos — chegaram no TypeScript 5.0 e seguem a proposta oficial do JavaScript. Não precisam de opção nenhuma; basta escrevê-los.
E não convivem: com experimentalDecorators ligado, o compilador usa as regras antigas em todo o ficheiro.
2. A forma nova, em duas partes
Um decorator é uma função que recebe o que está a decorar e um contexto, e devolve uma substituição — ou nada, se só quiser observar.
function medirTempo(
metodoOriginal: (...args: any[]) => any,
contexto: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
const inicio = performance.now();
const resultado = metodoOriginal.apply(this, args);
console.log(`${String(contexto.name)}: ${Math.round(performance.now() - inicio)}ms`);
return resultado;
};
}
class Relatorio {
@medirTempo
calcular(linhas: number[]) {
return linhas.reduce((soma, n) => soma + n, 0);
}
}
new Relatorio().calcular([1, 2, 3]); // calcular: 0msO contexto traz o nome, o tipo do que está a ser decorado (method, field, getter, class…), se é estático e se é privado.
3. O caso que aparece sempre: perder o this
Um método passado como callback perde o this. A solução de sempre é um bind no construtor; com um decorator, escreve-se uma vez e usa-se em todo o lado:
function ligado(_metodo: unknown, contexto: ClassMethodDecoratorContext) {
// Corre uma vez por instância, quando ela é criada
contexto.addInitializer(function (this: any) {
this[contexto.name] = this[contexto.name].bind(this);
});
}
class Botao {
etiqueta = 'Guardar';
@ligado
aoClicar() {
console.log(this.etiqueta); // 'Guardar', mesmo solto
}
}
const botao = new Botao();
document.addEventListener('click', botao.aoClicar); // já não rebentaO addInitializer é a peça mais útil do sistema novo: permite correr código na criação de cada instância, que é onde vive metade do que se quer fazer com decorators.
4. Um decorator com parâmetros
É só uma função que devolve o decorator:
function repetirEmErro(tentativas = 3) {
return function (metodo: (...args: any[]) => Promise<any>, _c: ClassMethodDecoratorContext) {
return async function (this: any, ...args: any[]) {
for (let i = 1; i <= tentativas; i++) {
try {
return await metodo.apply(this, args);
} catch (erro) {
if (i === tentativas) throw erro;
}
}
};
};
}
class Api {
@repetirEmErro(5)
async ler(url: string) {
return (await fetch(url)).json();
}
}5. Quando não usar
Esta é a parte que os exemplos costumam saltar. Um decorator esconde comportamento: o método que se lê não é o método que corre. Isso é excelente para atravessar muitas classes com a mesma preocupação — registo, cache, autorização, medição — e é mau para tudo o resto.
// Um decorator para isto é sintaxe a esconder uma função
@validar
guardar(dados: Dados) { ... }
// A função é mais clara, e vê-se onde é chamada
guardar(dados: Dados) {
const validado = validar(dados);
...
}A pergunta que resolve a decisão: isto repete-se em muitos sítios e é ortogonal ao que o método faz? Se sim, é um bom decorator. Se não, é uma função.
6. Duas notas práticas
Os decorators de parâmetro não existem no sistema novo. É por isso que frameworks com injeção de dependências — Angular, NestJS — continuam no
experimentalDecorators. Se trabalha com elas, é o antigo que tem de aprender.Isto é compilado. Antes de contar com decorators em Node ou no browser sem passo de compilação, confirme o suporte nativo na versão que vai usar — durante muito tempo foi o compilador a transformá-los, e a transformação não é gratuita nem sempre igual entre ferramentas.