Uma introdução prática à Decorators no TypeScript
Os decorators são um dos recursos mais poderosos oferecido pelo TypeScript, tendo como um dos principais objetivos ampliar funcionalidades de classes e métodos de forma simples e limpa. Atualmente, os decorators são uma proposta de estágio 2 para JavaScript e estão disponíveis como um recurso experimental no TypeScript. Mesmo sendo um recurso experimental, eles já estão presentes em grandes projetos de código aberto, como o Angular e Inversify.
Por ser um recurso experimental, para ser possível utilizar no TypeScript, é
necessário habilitar no tsconfig.json
:
|
|
Ou pela linha de comando:
|
|
Mesmo que inicialmente os decorators possam parecer mágicos, eles são simples de se entender e fáceis de se criar.
Mas afinal, o que é um decorator?
O site do TypeScript descreve como:
“Um Decorator é um tipo especial de declaração que pode ser anexada a uma declaração de classe, método, acessador, propriedade ou parâmetro.”
Essa definição pode não explicar muito o que um decorator realmente representa. Eu prefiro definir como “uma declaração especial para adicionar funcionalidades extras a uma declaração de classe, método, acessador, propriedade ou parâmetro”.
Você pode ter visto em algum projeto a utilização de decorators, eles utilizam
o formato @expression
, onde o valor expression
representa uma função que
fará as modificações as classes, métodos, acessadores, propriedades ou
parâmetros.
Para se criar um decorator é bem simples. Como já previamente explicado, os
decorators são apenas funções, essas funções são chamadas em tempo de
execução. Um exemplo bem simples é criarmos uma função log
que irá realizar um
console.log
no alvo em que ele for utilizado, ficando desta maneira:
|
|
Pode ser que em certas situações pode ser necessário você customizar como um decorator é aplicado à uma declaração. Para isso, é necessário criar um Decorator Factory, que é uma função que retorna a expressão que será executada. Seguindo o mesmo exemplo, imagine que agora você queira adicionar um prefixo estático nos logs, o resultado seria algo assim:
|
|
Como é possível analisar, agora ao invés de recebermos o alvo como parâmetro, recebemos o parâmetro que informamos e temos que retornar uma função, que será executada.
Tipos de decorators
Ao se desenvolver decorators é importante saber que existem vários tipos, esses tipos são determinados pelo alvo em que está sendo aplicado, sendo que cada tipo tem suas particularidades e assinaturas diferentes. Atualmente os tipos existentes são:
- Class Decorator.
- Property Decorator.
- Method Decorator.
- Accessor Decorator.
- Parameter Decorator.
Class Decorator
A maneira mais simples de se começar a entender os decorators é começar desenvolvendo para classes. Um decorator para classe deve ser declarado antes da declaração da classe. Esse decorator recebe um único parâmetro que é o construtor da classe alvo.
|
|
Caso o decorator retorne um valor, ele substituirá a declaração de classe pelo valor fornecido, que deve ser um construtor. Dessa maneira, diferente do exemplo acima, podemos aplicar mudanças diretas à classe, ao invés de apenas no protótipo da classe.
|
|
É importante ressaltar que caso você decida retornar um construtor, você deve manter a mesma assinatura do alvo.
Você perceberá no decorrer do aprendizado, que esse tipo de decorator é o mais generalista, pois nele você pode ter acesso à classe inteira, ao invés de pequenas partes do objeto.
Property Decorator
Um decorator de propriedade deve ser declarado antes da declaração da
propriedade. Dessa vez, o decorator, recebe 2 parâmetros, target
e key
. O
parâmetro target
é o protótipo da classe em que está sendo aplicado o
decorator, já o parâmetro key
é o nome da propriedade da classe em que está
sendo aplicado o decorator.
|
|
Com esse pequeno exemplo, foi mostrado na tela Task {} 'title'
, que representa
o protótipo da classe e o nome da propriedade.
Um ponto interessante e importante de se analisar, como já foi dito, recebemos como parâmetro o protótipo da classe e não sua instância, sabendo disso é possível ver no exemplo que o decorator foi executado mesmo sem instanciarmos a classe, isso por que o decorator é chamado no tempo de execução do arquivo. Isso deve ser levado em consideração na hora de se criar seus decorators já que você não terá uma chamada no decorator a cada vez que instanciar a classe.
O interessante desse tipo de decorator é a possibilidade de aplicar mudanças de comportamento nas propriedades.
|
|
No exemplo, criamos um decorator chamado logProperty
que tem como objetivo
fazer um console.log
toda vez que a propriedade tiver seu valor alterado ou
for acessada. Para saber o que acontece na propriedade, utilizamos os getters
e setters
do próprio JavaScript.
Method Decorator
Para muitos esse é o tipo de decorator mais útil oferecido pelo TypeScript. Um
decorator para métodos deve ser declarado antes da declaração do método. Ao se
utilizar um method decorator recebemos 3 parâmetros. O primeiro parâmetro é o
target
que é protótipo da classe, igual ao que vimos no property decorator.
O segundo parâmetro é o propertyKey
que é o nome do método em que estamos
aplicando. Já o último é o propertyDescriptor
, que é um conjunto de
propriedades que definem uma propriedade de um objeto em JavaScript, neste
objeto podemos ter acesso a propriedades como: configurable
, enumerable
,
value
e writable
, além de get
e set
. Tendo acesso nesses 3 parâmetros,
somos capazes de realizar quase qualquer operação em cima de nossos métodos.
Vamos imaginar um cenário onde temos um método changePassword
em uma classe
User
e queremos alterar o enumerable
deste método através de um decorator
para que esse método não apareça na hora de percorrer as propriedades existentes
na classe.
|
|
Neste simples exemplo, será mostrado na tela name
e changePassword
. Como
queremos alterar o enumerable
para o valor false
deste método para não
mostrar na tela, basta alteramos a propriedade dentro do nosso
propertyDescriptor
.
|
|
Agora será mostrado na tela apenas name
.
Esse tipo de decorator é extremamente útil quando queremos aplicar mudanças no comportamento dos nossos métodos e como temos acesso a quase tudo que representa o método, se torna muito simples aplicarmos as mudanças que queremos.
Accessor Decorator
Os accessor decoratos são os mesmos que os method decorators, mas são aplicados aos métodos setter ou getter.
|
|
É importante entender que o TypeScript não permite aplicar um decorator a ambos os acessadores de um único membro. Em vez disso, deve ser aplicado o decorator ao primeiro acessador especificado na ordem do objeto.
|
|
Parameter Decorator
Por último, mas não menos importante, temos os parameter decorators. Um
parameter decorator deve ser declarado antes da declaração de um parâmetro.
Esse decorator recebe 3 parâmetros. O primeiro, como na maioria dos decorators
que já vimos é o target
que é o protótipo da classe. O segundo é o
propertyKey
que é o nome do método que contém o parâmetro estamos trabalhando,
bem semelhante ao que ja vimos no method decorator. Já o último parâmetro, é o
parameterIndex
que é o número da posição do parâmetro na função, lembrando que
começa a partir do 0.
|
|
Dessa maneira como estamos construindo os nossos decorators, só é possível analisar o objeto e o método, qualquer alteração necessária no comportamento requer o uso do reflect-metadata (que é um assunto para outro post).
Quando usar
É comum quando estamos aprendendo algo novo entendermos como algo funciona mas dificilmente conseguiremos enxergar cenários para aquele aprendizado. Para alguns, não é diferente ao começar a aprender a trabalhar com os decorators.
Os decorators são extremamente úteis quando devemos adicionar ou alterar comportamento de nossos alvos através de meta-programação. Quando temos algo que pode ser considerado genérico, mas que pode ser reutilizado em vários lugares no objetivo de facilitar alguma mudança em cima do alvo, talvez seja uma ótima situação para se utilizar.
Ao se começar a pensar na criação de nossos próprios decorators podemos ver que um grande benefício é a reutilização, porém mesmo que isso seja uma verdade, devemos tomar muito cuidado para não acabar criando coisas extremamente complexas com várias responsabilidades e efeitos colaterais.
Espero que isso te ajude de alguma forma.