FNSP Web Page

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: true no tsconfig.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: 0ms

O 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 rebenta

O 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.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.