Entendendo os Decorators no TypeScript

Andrew Rosário
Inside PicPay
Published in
6 min readApr 14, 2023

Se você já trabalhou com frameworks que utilizam fortemente os recursos do TypeScript, como o Angular e o NestJS, com certeza já viu muitos decorators. Eles nada mais são do que um tipo especial de declaração que pode ser anexada em vários lugares, como por exemplo em classes, métodos, propriedades e parâmetros.

O decorator acaba sendo um recurso extremamente útil e elegante para incorporar qualquer tipo de comportamento em nosso código. Sua sintaxe é simples e bastante familiar (@).

E não poderia ter um momento mais apropriado para falar dos decorators do que este, pois com o lançamento da versão 5 do TypeScript, eles estão estáveis dentro da linguagem! Isso significa que não precisamos mais utilizar a flag experimentalDecorators .

Criando nosso primeiro decorator

Decorators nada mais são do que funções. Vamos criar nosso primeiro decorator com o exemplo mais simples de todos: o de realizar um console.log em uma classe:

function log(target) {
console.log(target);
}

@log
class User {}

// [Function: User]

Perceba que passamos um parâmetro target para a função. Este parâmetro é o prototype da classe que será anexada, então podemos realizar qualquer ação com ele.

Um ponto muito importante que precisamos entender e que muitos desenvolvedores acabam se confundindo: os decorators são sempre chamados quando uma classe é declarada e não quando um objeto é instanciado.

No exemplo abaixo podemos ter uma ideia do momento em que o decorator entra em ação. Por mais que realizemos a instância várias vezes da classe User, não é nesse momento em que veremos o console.log, e sim no momento de sua declaração, ou seja, quando ela for colocada em memória.

function log(target) {
console.log(target);
}

@log
class User {}

const user1 = new User();
const user2 = new User();
const user3 = new User();

Decorator Factory

Em muitos casos você irá querer customizar como um decorator é aplicado à uma declaração. Para isso, precisamos criar um Decorator Factory , que é uma função que retorna a expressão que será executada. Em outras palavras, é uma função que retorna outra função. Se essa expressão for nova pra você, recomendo ler sobre Higher Order Functions.

function log(prefix: string) {
return target => {
console.log(`${prefix} - ${target}`);
}
}

@log('Usuário')
class User {}
// Usuário - function User() {
// }

@log('Carro')
class Car {}
// Carro - function Car() {
// }

A vantagem aqui é que estamos livres para informar parâmetros para o decorator. Eles serão chamados pelo nosso decorator factory. Ao criar decorators em uma aplicação real, na maioria dos casos esse vai o ser tipo de declaração que você irá mais utilizar.

Class Decorator

Com estes exemplos iniciais já criamos um Class Decorator. Este é o tipo mais simples pois ele trabalha somente com o primeiro parâmetro target.

Vamos ver abaixo como podemos decorar uma classe com o intuito de adicionar novas propriedade para ela.

function deprecated(constructor) {
return class extends constructor {
deprecated = true;
message = 'Atenção! Esta classe está depreciada.';
};
}

@deprecated
class User {
constructor(private name: string) {}
}

console.log(new User('Andrew'));
/* {
deprecated: true,
message: 'Atenção! Esta classe está depreciada.',
name: 'Andrew'
} */

Temos a possibilidade de sobrescrever o construtor para atribuir o que quisermos! Neste momento já começamos a ter um gostinho do poder que o decorator nos oferece.

Method Decorator

Diferente do Class Decorator, o Method Decorator recebe 3 parâmetros. O primeiro parâmetro é o target que continua sendo a nossa classe. O segundo é a key, que é basicamente o nome do método em que estamos aplicando o decorator. Já o último é o propertyDescriptor, que é o método propriamente dito para que possamos manipulá-lo.

Para exemplificar, vamos decorar uma função de uma classe para que quando seja chamada, ela sofra um atraso de x milisegundos.

function timeout(milliseconds: number) {
return (target, key: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;

descriptor.value = function(...args) {
setTimeout(() => {
originalMethod.apply(this, args);
}, milliseconds);
};

return descriptor;

}
}

Mais uma vez estamos utilizando um decorator factory, neste caso para poder receber os milisegundos customizados. No parâmetro descriptor armazenamos em uma variável o método original pela propriedade value.

Podemos agora sobrepor o método original incluindo a lógica que quisermos. descriptor.value recebe uma função que tem como objetivo executar o método original (originalMethod.apply), porém aplicando um setTimeout com os milissegundos informados. O mais interessante é que a função sobreposta recebe todos os parâmetros (…args) da função original via Rest Parameters.

Não podemos nos esquecer de sempre retornar o descriptor após reprogramar o método. Caso contrário não teremos o efeito esperado.

Agora basta decorar um método qualquer que já teremos o resultado esperado:

class User {
constructor(private name: string) {}

@timeout(4000)
getName(): string {
return this.name;
}
}

Property Decorator

Para decorar propriedades, podemos criar os Property Decorators que recebem dois parâmetros: target e a propertyName.

Este tipo de decorator tem uma particularidade muito interessante: é possível interceptar seus getters e setters. Toda vez que a propriedade receber ou atribuir um novo valor, podemos executar uma ação neste momento.

Pense em uma aplicação frontend que precisa salvar e recuperar dados do LocalStorage diretamente do browser. Que tal realizar estas ações em um decorator?

function storage(key: string): PropertyDecorator {
return (target, propertyName: string) => {
let value = target[propertyName];

const getter = () => {
if (value) return value;
value = localStorage.getItem(key);
return value
};

const setter = (newValue) => {
value = newValue;
localStorage.setItem(key, newValue);
};

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

Criamos um decorator chamado storage que recebe por parâmetro uma key. Esta chave será armazenada no LocalStorage.

Primeiramente armazenamos o valor da propriedade na variável value. Depois criamos duas funções: getter e setter. A função getter verifica se o valor da propriedade não está vazio, caso contrário ela vai retornar o valor que está armazenado no LocalStorage. Já função setter simplesmente atribui o novo valor no LocalStorage.

Para finalizar, precisamos redefinir a propriedade com Object.defineProperty, passando para ela todas as novas configurações, incluindo nossas funções getter e setter.

class User {
@storage('userName')
name: string;

getName(): string {
// se estiver vazio retorna do LocalStorage
return this.name;
}

setName(name: string): void {
// atribui o novo nome e salva no LocalStorage
this.name = name;
}
}

A propriedade name da classe User agora tem super poderes! É como se houvesse uma espécie de middleware nela, interceptando suas ações.

Pense nas infinitas possibilidades do Property Decorator. Temos o poder de criar elos de ligação entre propriedades e evitar trazer lógicas de implementação pra dentro de suas classes.

Parameter Decorator

Por fim, temos o Parameter Decorator. Como o nome já diz, decoramos os parâmetros dos nossos métodos. Ele recebe 3 parâmetros. O primeiro, como na maioria dos decorators que já vimos é o target, ou seja, o protótipo da classe. O segundo é o propertyKey que contém o método atual que vamos trabalhar. Já o último parâmetro é o parameterIndex que nos retorna o número da posição do parâmetro na função, lembrando que se inicia a partir do 0.

function logParam() {
return (target, propertyKey: string, parameterIndex: number) => {
console.log('método atual:', propertyKey);
console.log('posição do parâmetro:', parameterIndex);
};
}

class User {
name: string;

setName(@logParam() name: string) {
this.name = name
}
}

// método atual: setName
// posição do parâmetro: 0

Dica de profissional: Podemos combinar Parameter Decorators com Method Decorators para construir personalizações ainda mais poderosas. Veja mais aqui.

Conclusão

Neste artigo podemos entender como funcionam os decorators no TypeScript e como eles podem ser úteis para adicionar ou alterar comportamentos em nosso código.

Vimos alguns exemplos com vários tipos de decorators diferentes, mas isto é somente uma introdução. Podemos ir muito mais a fundo em sua utilização. Portanto, não deixe de conferir a documentação oficial e também outros materiais relacionados a este assunto na web.

--

--

Andrew Rosário
Inside PicPay

Desenvolvedor Front-end, mentor e palestrante. Apaixonado por tecnologia e por compartilhar conhecimento.