Decorators

Decorators

Olá Leitor, seja bem vindo a mais uma lição da jornada do desenvolvedor Typescript 🥳

E hoje nós iremos ver um pouco sobre decorators, um dos assuntos mais "temidos" pelos desenvolvedores Typescript, devido a sua alta complexidade operacional.

Mas pode ficar tranquilo que aqui eu vou deixar tudo o mais mastigado possível, para que você entenda nos mínimos detalhes 🤓

O que é um decorator?

Também conhecidos como decoradores, eles são um recurso presente no Typescript, que nos permite adicionar anotações e comportamentos a classes e membros de classes (como métodos e propriedades).

Isso nos dá a possibilidade de executarmos diversas operações dentro dos nossos decorators.

Quando falamos em decorators, estamos falando também de metaprogramação, mas o que vem a ser uma metaprogramação?

O que é metaprogramação?

Metaprogramação é um conceito de programação onde o código é escrito para ler, gerar, modificar ou manipular outros códigos existentes.

Na prática, a metaprogramação envolve escrever programas que criam ou alteram outros programas, ou mesmo a si mesmos, durante a execução ou a compilação.

E não, a metaprogramação não se compara com as inteligências artificiais que nós temos atualmente, e muito menos tem haver com isso.

No final das contas, a metaprogramação nada mais é do que o uso de métodos e funções da própria linguagem, que nos permitem mudar métodos, variáveis e classes durante a execução do programa, por exemplo:

class MinhaClasse {
 minhaPropriedade: string = "Micilini Roll";
}

const instanciaDaClasse = new MinhaClasse();

console.log(Object.getOwnPropertyDescriptor(instanciaDaClasse, 'minhaPropriedade'));

No método Object.getOwnPropertyDescriptor , o sistema retorna um objeto que descreve a configuração de uma propriedade do objeto que instancia a classe (instanciaDaClasse).

Esse objeto contém as seguintes propriedades:

value: O valor da propriedade.

writable: Um booleano que indica se o valor da propriedade pode ser alterado.

enumerable: Um booleano que indica se a propriedade é enumerável (ou seja, se aparece em um loop for...in ou em Object.keys).

configurable: Um booleano que indica se a propriedade pode ser removida e se suas características podem ser alteradas.

Enfim, só com as funcionalidades acima, podemos notar um pouco do poder dos decorators em Typescript 😉

Partiu colocar a mão na massa?

Como funcionam os decorators?

Na prática, um decorator é aplicado por meio da sintaxe @decorator, que fica localizado acima de uma declaração de uma classe, método ou propriedade.

Essa sintaxe, serve para adicionarmos novas funcionalides (extras) a uma determinada classe.

Entretanto, para usarmos um decorator, nós precisamos fazer algumas modificações dentro do nosso arquivo de configurações (tsconfig.json).

Habilitando os decorators

Dentro do seu arquivo tsconfig.json, você vai precisar adicionar a seguinte configuração:

{
 "compilerOptions": {
 "target": "es6", // Define a versão do JavaScript alvo
 "module": "commonjs", // Define o sistema de módulos
 "experimentalDecorators": true, // Habilita o suporte a decoradores
 "emitDecoratorMetadata": true // Gera metadados de decoração para decoradores
 },
 "include": [
 "src/**/*" // Inclua seus arquivos de código-fonte aqui
 ],
 "exclude": [
 "node_modules" // Exclua a pasta de dependências
 ]
}

Note que as opções experimentalDecorators e, emitDecoratorMetadata, são configurações essenciais para garantir que o TypeScript reconheça, e compile corretamente o código que utiliza decorators, ok?

Feito isso, salva esse arquivo, e bora criar o nosso primeiro decorator 😉

Criando seu primeiro decorator (função)

No exemplo a seguir, nos iremos criar uma função bem simples que vai executar uma mensagem no console:

function meuDecorator(){
 console.log('Decorator Iniciado!');

 return function(target: any, propertKey: string, descriptor: PropertyDescriptor){
 console.log("Decorator sendo executado...");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

No código acima, criamos uma função bem simples chamada de meuDecorator(), que aciona o console que mostra a mensagem de inicialização, e um return que retorna uma função anônima que contém algumas características dos recursos de decorators do Typescript.

A função return function(target, propertyKey, descriptor), é o coração dos decorators, e poderá ser aplicada a uma classe, propriedade ou método alvo.

Isso significa que a função meuDecorator(), é como se fosse uma especie de módulo que será implementado dentro de uma classe, propriedade ou método.

target: representa o alvo ao qual o decorador será aplicado. Dependendo do tipo de decorador, eles podem ser:

  • Construtor da classe,
  • Protótipo da classe,
  • Objeto de uma propriedade

propertyKey: é o nome da propriedade ou o método que está sendo decorado.

descriptor: é o descritor de propriedade do método ou da própria propriedade em questão. Esse objeto contém metadados sobre a propriedade ou método, como value, writable, configurable, enumerable, etc.

Note que os argumentos target, propertyKey e descriptor nos dão informações do local em que aquele descorator foi executado.

Feito isso, agora você precisa inserir o seu decorator (meuDecorator()) em alguma classe, método ou propriedade 😊

Implementando um Decorator 

Com o seu decorador (meuDecorator()) em mãos, você vai precisar encaixá-lo em alguma lugar.

Para isso, vamos criar uma classe simples chamada de Animal que vai possuir o método existir():

class Animal{
 public existir(): void{
 console.log('Eu sou um Animal e eu existo!');
 }
}

const obj = new Animal();
obj.existir();//'Eu sou um Animal e eu existo!'

A classe acima apresenta um funcionamento bem simples, e não é nada muito diferente daquilo que vimos na lição sobre classes.

Vamos supor que nós queremos aplicar aquele nosso decorador dentro do método existir(). Para isso nós podemos fazer isso da seguinte forma:

function meuDecorator(){
 console.log('Decorator Iniciado!');

 return function(target: any, propertKey: string, descriptor: PropertyDescriptor){
 console.log("Decorator sendo executado...");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

class Animal{
 @meuDecorator()
 public existir(): void{
 console.log('Eu sou um Animal e eu existo!');
 }
}

const obj = new Animal();
obj.existir();//'Eu sou um Animal e eu existo!'

No caso do Typescript, por de baixo dos panos, ele subentende que o nosso decorator está associoado a função existir(), isso pelo fato dele estar sendo declarado em uma linha acima daquela função.

Se o @meuDecorator estivesse acima da classe, ele seria aplicado a classe, se ele estivesse acima de uma propriedade, ele seria aplicado à aquela propriedade.

Se você executar esse comando, vai receber as seguintes mensagens no console:

Note que o decorator em sí, só foi chamado quando executamos o método existir(), que está localizado dentro da classe Animal.

De acordo com as respostas obtidas no console, podemos notar que as operações declaradas dentro do método existir() foram executadas por último.

Fazendo com que toda a nossa lógica que declaramos dentro do nosso decorador (meuDecorator) fosse executadas em primeiro lugar.

E isso é um comportamento esperado, ok? Pois o Typescript sempre vai executar a lógica do nosso decorador, antes de partir para a lógica do próprio método.

Lembrando que podemos associar nossos decorators em métodos, propriedades e até mesmo dentro à própria classe.

Implementando decorators em propriedades

No tópico anterior, você viu como implementar um decorator dentro de um método de uma classe.

Com relação às propriedades, o nosso decorador precisa executar um tratamento diferente, pois as propriedades não contam com um descriptor.

Sendo assim, você vai precisar atualizar o seu decorador (meuDecorator) para que o parâmetro descriptor seja opcional. Vamos ver um exemplo:

function meuDecorator(){
 console.log('Decorator Iniciado!');

 return function(target: any, propertKey: string, descriptor?: PropertyDescriptor){
 console.log("Decorator sendo executado...");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

Seguindo a estratégia do código acima, nós conseguimos utilizar o meuDecorator tanto dentro de um método, quanto dentro de uma propriedade:

function meuDecorator(){
 console.log('Decorator Iniciado!');

 return function(target: any, propertKey: string, descriptor?: PropertyDescriptor){
 console.log("Decorator sendo executado...");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

class Animal{
 @meuDecorator()
 nome: string = "Micilini";

 @meuDecorator()
 public existir(): void{
 console.log('Eu sou um Animal e eu existo!');
 }
}

const obj = new Animal();

console.log(obj.nome);//Isso vai chamar o decorator
obj.nome = "Rolls";//E isso também

//obj.existir();//'Eu sou um Animal e eu existo!'

Lembrando que você pode definir um decorator especial para trabalhar especificamente com aquela propriedade, OK?

Pois como você irá aprender nos próximos tópicos, podemos usar múltiplos decorators dentro do nosso código 😉

Implementando múltiplos decorators

No Typescript, também é possível implementarmos múltiplos decorators, isto é, fazer com que uma propriedade, classe ou método execute um ou mais decoradores simultaneamente.

Vamos ver um exemplo disso funcionando na prática:

function meuDecoratorA1(){
 console.log('Decorator Iniciado! A1');

 return function(target: any, propertKey: string, descriptor: PropertyDescriptor){
 console.log("Decorator sendo executado... A1");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

function meuDecoratorA2(){
 console.log('Decorator Iniciado! A2');

 return function(target: any, propertKey: string, descriptor: PropertyDescriptor){
 console.log("Decorator sendo executado... A2");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

class Animal{
 @meuDecoratorA1()
 @meuDecoratorA2()
 public existir(): void{
 console.log('Eu sou um Animal e eu existo!');
 }
}

const obj = new Animal();
obj.existir();//'Eu sou um Animal e eu existo!'

Agora dê uma olhada no resultado do console:

Notou um comportamento singular? Sim, o Typescript sempre executa o decorador em ordem DECRESCENTE. Por esse motivo o MeuDecoratorA2 vai executar primeiro que o MeuDecoratorA1, pois ele está mais perto da declaração daquele método.

Lembrando que isso se aplica em classes e propriedades também.

Implementando decorators em classes

Por fim, nós também podemos implementar nossos decorators dentro das nossas classes, e o processo é bem simples, basta declará-lo acima da declaração da nossa classe 😎

Observação: seguindo a ideia das propriedades (que não contam com um descriptor), nossas classes também não contam com um propertKey, sendo assim, você também precisa torná-lo opcional.

function meuDecorator(){
 console.log('Decorator Iniciado!');

 return function(target: any, propertKey?: string, descriptor?: PropertyDescriptor){
 console.log("Decorator sendo executado...");
 console.log(target);
 console.log(propertKey);
 console.log(descriptor);
 }
}

@meuDecorator()
class Animal{
 @meuDecorator()
 nome: string = "Micilini";

 @meuDecorator()
 public existir(): void{
 console.log('Eu sou um Animal e eu existo!');
 }
}

//const obj = new Animal();

//console.log(obj.nome);//Isso vai chamar o decorator
//obj.nome = "Rolls";//E isso também

//obj.existir();//'Eu sou um Animal e eu existo!'

Note que nós nem precisamos instanciar a classe para o decorator executar as operações 🧐

Atenção: Se você quiser que um determinado decorator seja executado assim que uma classe for intânciada, basta declarar seu decorador em cima do método constructor.

Criando uma lógica por trás de um decorator

Lembra que em um tópico anterior, eu cheguei a mencionar que podemos executar diversas operações dentro de um decorator?

Observe um exemplo abaixo em que fazemos algumas manipulações no DOM, antes de executarmos o método existir():

function meuDecorator() {
 console.log('Decorator Iniciado!');

 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 console.log("Decorator sendo executado...");
 console.log(target);
 console.log(propertyKey);
 console.log(descriptor);

 // Salva a referência ao método original
 const originalMethod = descriptor.value;

 // Substitui o método original
 descriptor.value = function(...args: any[]) {
 // Manipulação do DOM
 const paragraph = document.createElement('p');
 paragraph.textContent = `Método ${propertyKey} chamado!`;
 document.body.appendChild(paragraph);

 // Chama o método original
 originalMethod.apply(this, args);
 };
 }
}

class Animal {
 nome: string = "Micilini";

 @meuDecorator()
 public existir(): void {
 console.log('Eu sou um Animal e eu existo!');
 }
}

// Para testar o código, você deve ter um documento HTML para manipular.
// Certifique-se de que o código esteja sendo executado em um ambiente onde
// o DOM está disponível (por exemplo, em um navegador).

const obj = new Animal();
obj.existir(); // Adicionará um parágrafo ao DOM e logará a mensagem no console

No código acima, estamos adicionando um parágrafo ao DOM antes do método existir() ser chamado.

No próximo exemplo, criamos um decorator chamado de enumerable que recebe como parâmetro um valor booleano, que é capaz de ativar ou desativar a propriedade enumerable de um método:

function enumerable(value: boolean) {
 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 // Altera a propriedade enumerable do descritor do método
 descriptor.enumerable = value;

 // Log para depuração
 console.log(`Decorator aplicado ao método ${propertyKey}`);
 console.log(`Enumerable: ${value}`);
 };
}

class Animal {
 @enumerable(true)
 public existir(): void {
 console.log('Eu sou um Animal e eu existo!');
 }

 @enumerable(false)
 public esconder(): void {
 console.log('Eu sou um Animal e eu me escondo!');
 }
}

// Para testar a configuração enumerable, você pode usar o seguinte código

const obj = new Animal();

// Verifica quais métodos são enumeráveis
console.log(Object.keys(obj)); // Deve listar apenas métodos com enumerable: true

// Para depuração, pode ser útil verificar a configuração dos descritores
console.log(Object.getOwnPropertyDescriptor(Animal.prototype, 'existir')); // Deve mostrar enumerable: true
console.log(Object.getOwnPropertyDescriptor(Animal.prototype, 'esconder')); // Deve mostrar enumerable: false

Implementado Accessor Decorators (para métodos getters e setters)

Sim, existem decoradores especiais que são feitos para serem usados dentro dos métodos getters e setters.

Vejamos como eles funcionam na prática:

// Decorator para o getter
function getterDecorator() {
 console.log('Getter Decorator Iniciado!');

 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 console.log("Getter Decorator sendo executado...");
 console.log(target);
 console.log(propertyKey);
 console.log(descriptor);

 const originalMethod = descriptor.get;
 descriptor.get = function() {
 console.log(`Getter para ${propertyKey} chamado`);
 return originalMethod?.apply(this);
 };
 };
}

// Decorator para o setter
function setterDecorator() {
 console.log('Setter Decorator Iniciado!');

 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 console.log("Setter Decorator sendo executado...");
 console.log(target);
 console.log(propertyKey);
 console.log(descriptor);

 const originalSetMethod = descriptor.set;
 descriptor.set = function(value: any) {
 console.log(`Setter para ${propertyKey} chamado com valor ${value}`);
 originalSetMethod?.apply(this, [value]);
 };
 };
}

class Animal {
 private _nome: string = "Micilini";

 @getterDecorator()
 get nome(): string {
 return this._nome;
 }

 @setterDecorator()
 set nome(value: string) {
 this._nome = value;
 }
}

// Testando a classe e os decorators
const animal = new Animal();
console.log(animal.nome); // Espera-se que o getter seja chamado e logue a mensagem
animal.nome = "Luna"; // Espera-se que o setter seja chamado e logue a mensagem
console.log(animal.nome); // Espera-se que o getter seja chamado e logue a mensagem

No exemplo acima, eu criei dois decorators, uma para trabalhar exclusivamente com o método getter e outro para o setter.

Dentro do comando acima, nós vimos alguns novos comandos especiais, vamos aprender separadamente o que cada um deles realiza:

const originalMethod = descriptor.get;

O descriptor.get, está salvando uma referência ao método getter original (o método que estava definido antes de aplicar o decorator), fazendo com que ele contenha o getter original associado à propriedade.

descriptor.get = function() {
 console.log(`Getter para ${propertyKey} chamado`);
 return originalMethod?.apply(this);
};

originalMethod é o getter original que foi salvo na variável anterior.

A chamada originalMethod?.apply(this) invoca o getter original, preservando o contexto de execução (this) da instância da classe onde o getter está sendo chamado.

O operador ?. (optional chaining) é usado para garantir que originalMethod não seja undefined antes de tentar chamá-lo.

Agora, se o comando originalMethod não estiver definido, o código não tentará chamá-lo e retornará undefined.

A mesma lógica pode ser aplicada também no descriptor.set;. A diferença é que estamos chamando o setter original repassando o valor que foir recebido:

originalSetMethod?.apply(this, [value]);

Isso significa que você pode modificar o valor quando um método setter é chamado. Observe um exemplo abaixo de como isso pode ser feito:

// Decorator para o setter
function setterDecorator() {
 console.log('Setter Decorator Iniciado!');

 return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 console.log("Setter Decorator sendo executado...");
 console.log(target);
 console.log(propertyKey);
 console.log(descriptor);

 const originalSetMethod = descriptor.set;
 descriptor.set = function(value: any) {
 // Modificando o valor antes de passá-lo ao setter original
 const newValue = `Modificado: ${value}`;

 console.log(`Setter para ${propertyKey} chamado com valor ${newValue}`);
 
 // Chama o método setter original com o valor modificado
 originalSetMethod?.apply(this, [newValue]);
 };
 };
}

class Animal {
 private _nome: string = "Micilini";

 @setterDecorator()
 set nome(value: string) {
 this._nome = value;
 }

 get nome(): string {
 return this._nome;
 }
}

// Testando a classe e o decorator
const animal = new Animal();
animal.nome = "Luna"; // Espera-se que o setter seja chamado com o valor "Modificado: Luna"
console.log(animal.nome); // Deve exibir "Modificado: Luna"

Incrível, não acha?

Criando formatações para uma propriedade usando decorators

Vamos supor que você tem uma classe chamada Produto, que conta com uma propriedade chamada preco:

class Produto {
 preco: number;

 constructor(preco: number) {
 this.preco = preco;
 }
}

const produto = new Produto(1234.56);
console.log(produto.preco); // R$ 1.234,56

Considerando que você queira garantir que os valores atribuídos a propriedade preco sejam sempre numéricos, você precisará criar um decorator capaz de converter este valor. Observe:

function formatNumber(target: any, propertyKey: string) {
 // Define a função que irá formatar o número
 const formatter = new Intl.NumberFormat('pt-BR', {
 style: 'currency',
 currency: 'BRL',
 });

 // Cria um getter e setter para a propriedade
 let value: number = target[propertyKey];

 const getter = () => {
 return formatter.format(value);
 };

 const setter = (newValue: number) => {
 value = newValue;
 };

 Object.defineProperty(target, propertyKey, {
 get: getter,
 set: setter,
 enumerable: true,
 configurable: true,
 });
}

No código acima, estamos definindo um decorator, que nada mais é do que uma função que recebe dois parâmetros: target (o protótipo da classe) e propertyKey (o nome da propriedade).

Após isso, estamos definindo uma função formatter usando Intl.NumberFormat para formatar o número como moeda.

Finalizando com um Object.defineProperty para definir um getter e um setter para a propriedade decorada. O getter formata o valor e o setter simplesmente atualiza o valor.

Vejamos o código completo:

function formatNumber(target: any, propertyKey: string) {
 // Define a função que irá formatar o número
 const formatter = new Intl.NumberFormat('pt-BR', {
 style: 'currency',
 currency: 'BRL',
 });

 // Cria um getter e setter para a propriedade
 let value: number = target[propertyKey];

 const getter = () => {
 return formatter.format(value);
 };

 const setter = (newValue: number) => {
 value = newValue;
 };

 Object.defineProperty(target, propertyKey, {
 get: getter,
 set: setter,
 enumerable: true,
 configurable: true,
 });
}

class Produto {
 @formatNumber
 preco: number;

 constructor(preco: number) {
 this.preco = preco;
 }
}

const produto = new Produto(1234.56);
console.log(produto.preco); // R$ 1.234,56

Arquivos da lição

Os arquivos desta lição podem ser encontrados no repositório do GitHub por meio deste link.

Conclusão

Nesta lição você aprendeu um importante recurso que pode ser usado para a validação e atualização dos nossos objetos, os decorators.

Te aguardo na próxima lição 😉