Decorators no JavaScript

Lucas Santos - Mar 13 '23 - - Dev Community

Decorators são uma das mais antigas propostas do JavaScript. Quantas vezes você já não ouviu que "O JavaScript vai ter decorators em breve"? Mas o que são esses decorators e o que eles vão mudar na nossa vida? Hoje ela está em estágio 3, o que significa que o tempo para ela ir ao ar reduz drásticamente, mas ainda sim não temos uma resposta concreta.

Se você não sabe como funciona o JavaScript, nesse vídeo eu explico um pouco mais sobre o processo de lançamento de novas funcionalidades do JavaScript, se você ainda não assistiu, eu recomendo fortemente para poder entender melhor como tudo funciona!

Decorators

Decorators são o nome curto para as decorator functions , que é um padrão de projeto, inclusive. Eles são uma função (ou método) que modificam o comportamento de outra função passada, retornando uma nova função.

Essencialmente você pode implementar decorators em qualquer linguagem, afinal eles são um padrão de projetos. No JavaScript você poderiam implementar um decorator da seguinte forma:

const decorator = (fn) => {
  return (...params) => {
    console.log('antes da função')
    const resultado = fn.call(this, ...params)
    console.log('depois da função')
    return resultado
  }
}

const func = (nome) => console.log(`Olá ${nome}`)
const decorada = decorator(func)
decorada('Lucas')
// antes da função
// Olá Lucas
// depois da função
Enter fullscreen mode Exit fullscreen mode

Porém, algumas linguagens possuem uma sintaxe especial para chamar decorators, como Python e Java, por exemplo, veja como podemos criar um decorator em Python:

def decorator(fn):
    def wrap():
        print("antes da função")
        fn()
        print("depois da função")
    return wrap

@decorator
def sayHello():
    print("hello!")

sayHello()

# antes da função
# hello!
# depois da função
Enter fullscreen mode Exit fullscreen mode

Percebe que temos um @decorator? Essa é a sintaxe mais utilizada para chamarmos um decorator na função que vem logo em seguida.

A maioria das linguagens permite que decorators sejam aplicados em diversos locais, como classes, métodos, propriedades e etc. No JavaScript esse não foi sempre o caso, a versão anterior da proposta (que estava no estágio 2) dizia que os decorators só poderiam ser aplicados à classes e a nenhum outro tipo de objeto.

Com a nova proposta, os decorators podem ser aplicados nos seguintes tipos de objetos:

  • Classes (como já eram aplicados antes)
  • Propriedades de classes
  • Métodos de classes
  • Acessores de classes

Ou seja, ainda estamos focando na classe, mas não é mais somente na instância da classe, mas sim em tudo que vem dentro dela.

Usando decorators

Decorators são essencialmente funções, como vimos antes. Todas essas funções vão levar dois parâmetros:

  1. O valor que está sendo decorado, que é o elemento que aquele decorator está aplicado
  2. Um objeto de contexto, contendo informações sobre o valor decorado

Tenha em mente que o valor decorado é uma referência para o objeto original, ou seja, qualquer mudança nesse valor, vai interferir com o valor original.

O tipo declarado (tirado da proposta) é exatamente esse:

type Decorator = (value: Input, context: {
  kind: string;
  name: string | symbol;
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  private?: boolean;
  static?: boolean;
  addInitializer?(initializer: () => void): void;
}) => Output | void;
Enter fullscreen mode Exit fullscreen mode

Nesse tipo, Input e Output representam, respectivamente, o objeto que você está decorando e o retorno do decorator, que é uma função. Cada tipo de decorator pode retornar um tipo de função diferente e tem um tipo de input diferente, seja ele um decorator de classe, propriedade ou acessor.

O objeto de contexto também varia de acordo com o valor que você está decorando, então ele pode ou não pode conter alguns dos campos, por exemplo, o campo access só existe para acessores.

As demais propriedades tem valores bem fixos, por exemplo:

  • kind é o tipo do objeto que você está decorando, essa propriedade existe basicamente para verificar se você está usando o decorator corretamente e buscando as propriedades corretas. Os valores possíveis são: class, method, getter, setter, field e accessor
  • name é o nome do objeto decorado, no caso de elementos privados vai ser a descrição (que é o próprio nome da propriedade)
  • access um objeto que contém duas possíveis chaves get e set que são funções usadas para acessar o valor decorado. É importante notar que esses valores são os valores finais que são passados para a instância do objeto, e não o valor que foi passado para o decorator
  • static indica se o valor é um elemento estático de uma classe, portanto só se aplica para elementos que podem ser estáticos
  • private indica se o elemento é privado, e tem a mesma regra do static
  • addInitializer é uma função extra que permite que você adicione uma lógica de inicialização do objeto decorado, todos os tipos possuem essa funcionalidade e ele opera por classe e não por instância , ou seja, ele só vai executar em objetos que não tem kind === 'field'

Ordem de aplicação

Assim como todos os elementos que suportam vários tipos de uso, os decorators são aplicados em uma ordem.

Primeiro eles só são aplicados quando todos foram chamados. Depois disso, os decorators são aplicados da menor ordem para a maior ordem, isso significa que primeiramente todos os decorators de métodos e campos são chamados e aplicados e depois os decorators de classe são aplicados e, por fim, todos os decorators de campos estáticos são aplicados.

Além disso, não existem regras especiais quanto a qual tipo de função pode ser usada como um decorator, desde que ela siga a assinatura proposta, qualquer função pode ser aplicada como um decorator.

Tipos de decorators

Vamos agora passar um por um com os tipos de decorators que temos nessa proposta, começando com a maior ordem e descendo para as ordens menores.

Class methods

Decorators aplicados em métodos de classe como a seguir:

class foo {
    @dec
    metodo (arg) {}
}
Enter fullscreen mode Exit fullscreen mode

Esse tipo de decorator segue a seguinte tipagem:

type ClassMethodDecorator = (value: Function, context: {
  kind: "method";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;
Enter fullscreen mode Exit fullscreen mode

Veja que o kind vai ser sempre method e o acessor vai ter somente o método get, uma vez que você não pode dar set em um método.

O parâmetro value é o método que está sendo decorado, além disso o decorator pode ou não retornar um novo método que vai substituir o método que está sendo decorado, se ele não retornar nada então o método será executado normalmente.

Um exemplo clássico é o decorator de log que mostrei no início do artigo, podemos criar um novo decorator para realizar um log do que está sendo executado pelo método, executar o método em si e depois retornar o resultado:

function debug(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`começando ${name} com os argumentos ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`fim de ${name}`);
      return ret;
    };
  }
}

class C {
  @debug
  m(arg) {}
}

new C().m(1);
// começando m com os argumentos 1
// fim de m
Enter fullscreen mode Exit fullscreen mode

Veja que neste caso estamos retornando uma função que vai substituir o método na classe original (o protótipo vai ser substituído), se não retornássemos nada, somente o decorator seria executado.

Se quiséssemos fazer isso sem usar os decorators, podemos imaginar que temos a classe e estamos substituindo o método m no protótipo diretamente pela chamada com o nosso decorator:

class C {
    m(arg) {}
}

C.prototype.m = debug(C.prototype.m, { kind: 'method', name: 'm' }) ?? C.prototype.m
Enter fullscreen mode Exit fullscreen mode

Acessores de classe

Os acessores de classes (como get e set) podem ter duas assinaturas dependendo do tipo de acessor que estamos falando, para get:

type ClassGetterDecorator = (value: Function, context: {
  kind: "getter";
  name: string | symbol;
  access: { get(): unknown };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;
Enter fullscreen mode Exit fullscreen mode

E para o set a diferença é que o kind vai ser setter e vamos ter um access com uma função set:

type ClassSetterDecorator = (value: Function, context: {
  kind: "setter";
  name: string | symbol;
  access: { set(value: unknown): void };
  static: boolean;
  private: boolean;
  addInitializer(initializer: () => void): void;
}) => Function | void;
Enter fullscreen mode Exit fullscreen mode

O funcionamento é exatamente igual aos decorators de métodos, porém é importante notar que os decorators de acessores são aplicados separadamente para getters e setters ou seja:

class C {
  @foo
  get x() {
    // ...
  }

  set x(val) {
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Nessa classe, o decorator está decorando apenas o get x() e não o set x(val). Eles são tão iguais que podemos reutilizar a mesma função debug que tínhamos anteriormente, só precisamos tratar os novos tipos de kind:

function debug(value, { kind, name }) {
  if (['method', 'getter', 'setter'].contains(kind)) {
    return function (...args) {
      console.log(`começando ${name} com os argumentos ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`fim de ${name}`);
      return ret;
    };
  }
}

class C {
  @debug
  set x(arg) {}
}

new C().x = 1
// começando x com os argumentos 1
// fim de x
Enter fullscreen mode Exit fullscreen mode

Da mesma forma, podemos aplicar essa funcionalidade sem o uso de decorators usando Object.defineProperty:

class C {
  set x(arg) {}
}

let { set } = Object.getOwnPropertyDescriptor(C.prototype, "x");
set = debug(set, {
  kind: "setter",
  name: "x",
  static: false,
  private: false,
}) ?? set;

Object.defineProperty(C.prototype, "x", { set });
Enter fullscreen mode Exit fullscreen mode

Propriedades de classe (class fields)

Esse tipo de decorator usa a tipagem completa:

type ClassFieldDecorator = (value: undefined, context: {
  kind: "field";
  name: string | symbol;
  access: { get(): unknown, set(value: unknown): void };
  static: boolean;
  private: boolean;
}) => (initialValue: unknown) => unknown | void;
Enter fullscreen mode Exit fullscreen mode

Ele possui tanto os acessores get quanto o set, além de possuir as propriedades static e private, porém, diferente dos demais, ele não possui um método addInitializer já que propriedades não podem ser inicializadas dessa forma.

Também, diferente dos demais tipos de decorators, como propriedades não tem um valor direto de input, portanto o value é sempre undefined ou seja, você não recebe a propriedade e nem uma referência dela, ao invés disso você pode retornar uma função que recebe o valor inicial e retorna um novo valor sempre que a propriedade é atribuída.

Para podermos usar a nossa função de debug nesses casos, vamos precisar de uma pequena modificação, já que não podemos retornar uma função nova mas sim um valor inicial.

function debug (_, {kind, name}) {
    if (king === 'field') {
        return function (initialValue) {
             console.log(`inicializando variável ${name} com valor ${initialValue}`)
            return initialValue
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

E então podemos usar o nosso campo da seguinte maneira:

class C {
    @debug x = 1
}

new C()
// Inicializando variável x com valor 1
Enter fullscreen mode Exit fullscreen mode

E podemos implementar esse mesmo comportamento usando uma chamada de inicialização na propriedade:

const inicializarX = debug(undefined, { kind: 'field', name: 'x' }) ?? (initialValue) => initialValue

class C {
    x = inicializarX.call(this, 1)
}
Enter fullscreen mode Exit fullscreen mode

Um dos exemplos interessantes que a própria proposta apresenta é que, como a função de inicialização é chamada com a instância da classe como this, então esse tipo de decorator pode ser utilizado para criar relações de inicialização, tipo registrar uma classe filha em uma classe pai como é mostrado no exemplo abaixo:

const CHILDREN = new WeakMap();

function registerChild(parent, child) {
  let children = CHILDREN.get(parent);

  if (children === undefined) {
    children = [];
    CHILDREN.set(parent, children);
  }

  children.push(child);
}

function getChildren(parent) {
  return CHILDREN.get(parent);
}

function register() {
  return function(value) {
    registerChild(this, value);

    return value;
  }
}

class Child {}
class OtherChild {}

class Parent {
  @register child1 = new Child();
  @register child2 = new OtherChild();
}

let parent = new Parent();
getChildren(parent); // [Child, OtherChild]
Enter fullscreen mode Exit fullscreen mode

Claro que você também pode usar uma lista de classes filhas interna da classe pai, por exemplo, para registrar injeção de dependências.

Classes

O último tipo de decorator é também um dos mais comuns, o decorator de classe. Ele segue uma versão simplificada da interface:

type ClassDecorator = (value: Function, context: {
  kind: "class";
  name: string | undefined;
  addInitializer(initializer: () => void): void;
}) => Function | void;
Enter fullscreen mode Exit fullscreen mode

A grande diferença além do kind é que não temos métodos acessores e também não temos as propriedades para privada e estática.

O primeiro parâmetro vai ser sempre a classe que está sendo decorada e ele pode retornar um novo objeto do tipo callable, que é uma função, uma classe, um Proxy ou qualquer outra coisa que possa ser invocada.

Um exemplo, é estendermos o construtor de uma classe para que possamos incluir uma chamada para um console sempre que uma nova classe for invocada:

function debug (value, {kind, name}) {
    if (kind === 'class') {
        return class extends value {
            constructor (...args) {
                super(...args)
                console.log(`construindo uma nova instancia de ${name} com os argumentos ${args.join(', ')}`)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

E usarmos na nossa classe dessa forma:

@debug
class C {}

new C(1)
// construindo uma nova instancia de C com os argumentos 1
Enter fullscreen mode Exit fullscreen mode

Essencialmente podemos fazer o mesmo da seguinte forma sem decorators:

class C {}

C = debug(C, {kind: 'class', name: 'C'}) ?? C
new C(1)
Enter fullscreen mode Exit fullscreen mode

Auto accessors

Juntamente com a proposta de decorators, esse documento também propõe um outro elemento da sintaxe chamado auto accessors. Hoje podemos declarar acessores da seguinte forma:

class foo {
    #privado = true

    get getPrivado () { return this.#privado }
    set setPrivado (val) { this.#privado = val }
}
Enter fullscreen mode Exit fullscreen mode

Assim vamos ter uma propriedade getPrivado e uma setPrivado para poder acessar propriedades privadas dentro de classes, o que é bem útil quando temos que fazer algum tratamento de dados ou então setar algum tipo de informação que exija algum processamento prévio.

O que a proposta apresenta é a nova keyword accessor, que vão fazer as seguintes operações:

  1. Criar uma propriedade privada de mesmo nome dentro da classe
  2. Criar um acessor get e um acessor set para essa propriedade com o mesmo nome

No final teremos uma sintaxe como essa:

class C {
    acessor x = 1
}

const c = new C()
c.x // 1
c.x = 2
c.x // 2
Enter fullscreen mode Exit fullscreen mode

Isso é o mesmo do que fazermos:

class C {
    #x = 1

    get x() {
        return this.#x
    }

    set (val) {
        this.#x = val
    }
}
Enter fullscreen mode Exit fullscreen mode

Um detalhe é que também podemos ter acessores privados:

class C {
    accessor #x = 2
}
Enter fullscreen mode Exit fullscreen mode

Ao meu ver, a proposta apresenta os auto-accessors como uma forma de contornar o problema de que não podemos setar um decorator automaticamente para um get e um set, como expliquei antes, então teríamos que chamar duas vezes a mesma função para, essencialmente, a mesma variável.

Os auto-accessors usam uma versão um pouco diferente da interface:

type ClassAutoAccessorDecorator = (
  value: {
    get: () => unknown;
    set(value: unknown) => void;
  },
  context: {
    kind: "accessor";
    name: string | symbol;
    access: { get(): unknown, set(value: unknown): void };
    static: boolean;
    private: boolean;
    addInitializer(initializer: () => void): void;
  }
) => {
  get?: () => unknown;
  set?: (value: unknown) => void;
  init?: (initialValue: unknown) => unknown;
} | void;
Enter fullscreen mode Exit fullscreen mode

Como você pode ver, o valor que recebemos no primeiro parâmetro é um objeto com os dois acessores da propriedade. O objeto de contexto recebe um kind como accessor, a propriedade access com ambas as funções get e set e as demais propriedades que estamos vendo nas outras interfaces.

A questão é que, para o primeiro parâmetro, vamos receber o objeto com os dois acessores que estão definidos no protótipo da classe , ou seja, é o próprio objeto de acesso que a a classe vai ter. No caso de termos um acessor estático, vamos receber a própria classe.

Esse objeto existe para que o decorator possa criar um wrap em volta deles e retornar um novo get e/ou um novo set, essencialmente criando um proxy que intercepta as chamadas para qualquer um desses acessores. O que não é possível com propriedades de classe normalmente.

Adicionalmente, quando retornamos o objeto com as propriedades, também podemos retornar uma função init que é uma função de inicialização que pode ser utilizada para mudar o valor inicial da variável privada que está setada na classe. Se você retornar o objeto sem qualquer um dos valores, seja get, set ou init o valor original do acessor vai ser usado.

Criando um exemplo com nosso decorator de debug, podemos fazer uma extensão para ele trabalhar com os auto-accessors:

function debug (target, {kind, name}) {
  if (kind === 'accessor') {
    const {get, set} = target
    return {
      get() {
        console.log(`get ${name}`)
        return get.call(this)
      },
      set(val) {
        console.log(`set ${name} para ${val}`)
        return set.call(this, val)
      },
      init (initialValue) {
        console.log(`iniciando ${name} com o valor ${initialValue}`)
        return initialValue
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Como você pode perceber, auto-accessors são um pouco mais longos de trabalhar porque você precisa retornar um objeto de funções, mas não é nada muito além do que já fizemos aqui na maioria dos outros casos.

Depois podemos usá-los da seguinte forma:

class C {
  @debug accessor x = false
}

const c = new C()
// iniciando x com o valor false
c.x
// get x
c.x = true
// set x para true
c.x 
// get x
Enter fullscreen mode Exit fullscreen mode

Se quisermos fazer a mesma coisa sem os decorators, vamos usar uma mistura do que temos nas propriedades e nos acessores que já fizemos antes:

class C {
  #x = inicializarX.call(this, 1);

  get x() {
    return this.#x;
  }

  set x(val) {
    this.#x = val;
  }
}

let { get: oldGet, set: oldSet } = Object.getOwnPropertyDescriptor(C.prototype, "x");

let {
  get: newGet = oldGet,
  set: newSet = oldSet,
  init: initializeX = (initialValue) => initialValue
} = logged(
  { get: oldGet, set: oldSet },
  {
    kind: "accessor",
    name: "x",
    static: false,
    private: false,
  }
) ?? {};

Object.defineProperty(C.prototype, "x", { get: newGet, set: newSet });
Enter fullscreen mode Exit fullscreen mode

addInitializer e inicialização de contexto

O método addInitializer que vimos em algumas das interfaces no objeto de contexto de todos os decorators, exceto o de classe, é um método que pode ser chamado para associar uma função de inicialização com a classe ou o elemento que estamos decorando.

Esse método pode ser usado para rodar qualquer código depois que o valor já foi setado permitindo que você possa finalizar a inicialização desse valor. Porém, a ordem de execução desses inicializadores depende do decorator que estamos usando:

  • Para classes , os inicializadores rodam depois que a classe foi completamente definida, depois de todas as propriedades estáticas serem atribuídas
  • Para elementos de classe (class elements), os inicializadores rodam durante a construção, mas antes da inicialização das propriedades da classe
  • Para elementos estáticos , os inicializadores rodam também durante a inicialização da classe, antes dos campos estáticos serem definidos, mas depois que todos os elementos de classe foram definidos

Alguns exemplos que a proposta apresenta.

@customElement

Podemos usar o addInitializer para poder decorar uma classe que vai registrar um novo webComponent no browser:

function customElement (name) {
    return (value, { addInitializer }) => {
        addInitializer(function() {
            customElements.define(name, this)
        })
    }
}

@customElement('elemento')
class Elemento extends HTMLElement {
    static get observedAttributes() {
        return ['attr', 'att']
    }
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, perceba que podemos "decorar" um decorator fazendo um wrap com uma outra função para que possamos passar parâmetros para ele, nesse caso estamos querendo passar a o nome do elemento para o decorator, então podemos criar uma função que recebe o nome e retorna uma outra função com a mesma assinatura do decorator.

@bound

Um decorator que é aplicado em um método de uma classe para poder modificar o seu this para o this daquela classe:

function bound (value, {name, addInitializer}) {
    addInitializer(function () {
        this[name] = this[name].bind(this)
    })
}

class C {
    message = 'oi!'

    @bound
    m() {
        console.log(this.message)
    }
}

const {m} = new C()
m() // oi!
Enter fullscreen mode Exit fullscreen mode

Perceba que, em ambos os casos, estamos usando function() dentro de addInitializer, isto porque queremos manter o this daquele escopo como sendo o escopo do decorator, isso é mais evidente nesse exemplo, mas também vale para o @customElement

Acessores de contexto

Um objeto que não utilizamos aqui foi o objeto access que vem de dentro dos contextos dos decorators.

Um exemplo muito útil é a criação de um contâiner de injeção de dependências. Que é uma ferramenta muito útil para poder criar automaticamente instâncias de classes dependentes para classes que levam essas dependências, dessa forma você não precisa passar todas as dependências como parâmetros.

Isso já é uma realidade com a biblioteca TSyringe feita pela Microsoft para demonstrar o poder dos decorators no TypeScript.

Essencialmente o que precisamos fazer é ter uma lista global de classes e suas dependências:

const INJETAVEIS = new WeakMap()

function initContainer() {
    const injecoes = []

    function injetavel (Class) {
        INJETAVEIS.set(Class, injecoes)
    }

    function injetar (chave) {
        return function aplicarDependencia (alvo, contexto) {
            injecoes.push({ chave, set: context.access.set })
        }
    }

    return { injetavel, injetar }
}
Enter fullscreen mode Exit fullscreen mode

Essa função vai inicializar a nossa lista global de dependências para uma determinada classe, então o que precisamos fazer é anotar a classe que queremos automatizar com @injectable e as dependências dessa classe com @inject. Mas antes precisamos de um container que vai ser a nossa instância global que vai ler dessa lista:

class Container {
    registro = new Map()

    registrar (nome, valor) {
        this.registro.set(nome, valor)
    }

    buscar (nome) {
        return this.registry.get(nome)
    }

    criar (Classe) {
        const instancia = new Classe()

        for (const { chave, set } of INJETAVEIS.get(Classe) || []) {
            set.call(instancia, this.buscar(chave))
        }

        return instancia
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui o que estamos fazendo é criando um container que vai registrar as dependências globais, ou seja, todas as classes que instanciamos uma vez, esse registro vai ter o nome que quisermos dar e também a instância da classe que criamos.

Quando definirmos uma nova classe através do container usando criar, vamos passar o construtor da classe que queremos criar, depois, vamos buscar todas as classes injetáveis que batem com essa descrição na nossa lista global e vamos chamar o método set para setar a uma nova propriedade na classe.

Quando chamamos set.call(instancia, this.buscar(chave)) estamos dizendo que queremos que a propriedade anotada chame seu acessor set com o this definido para a nova instância da classe que criamos, com o valor sendo a classe dependente que já instanciamos anteriormente.

Vamos dar um exemplo:

class Store {}

const { injetavel, injetar } = initContainer()

// A classe C é injetável e pode receber dependências externas
@injetavel
class C {
    // Essa propriedade é a instancia guardada na chave
    // nomeDaclasse que registramos no container
    @injetar('nomeDaClasse') store
}

const container = new Container()
const store = new Store()

// Registrando a Store no container como uma dependência
container.register('nomeDaclasse', store)

const c = container.create(C)
c.store === store // true
Enter fullscreen mode Exit fullscreen mode

Veja que estamos usando container.create(C) para criar uma nova classe com as dependências já injetadas, mas isso não é totalmente necessário, como você pode ver nessa documentação do TSyringe e como já mostrei antes, podemos usar os decorators para substituir completamente o construtor da classe e rodar essa lógica automaticamente para todas as dependências de uma mesma classe.

Testando você mesmo

Se você quiser rodar qualquer um dos códigos que eu coloquei por aqui, mesmo antes da proposta estar completamente publicada e disponível, isso é possível através de transpiladores como o babel.

Para isso, crie uma nova pasta em qualquer lugar e rode npm init -y (lembrando que você precisa ter o Node e o NPM instalados), isso vai criar um novo arquivo package.json, depois execute o comando npm i -D @babel/cli @babel/core @babel/plugin-proposal-decorators @babel/preset-env.

Abra o arquivo package.json e, na sessão scripts, adicione um novo script transpile:

{
    "scripts": {
        "transpile": "babel src -d dist"
    }
}
Enter fullscreen mode Exit fullscreen mode

Esse script vai pegar qualquer código .js dentro da pasta src e vai transpilar para um novo arquivo na pasta dist.

Agora crie um novo arquivo chamado babel.config.json com esse conteúdo:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "version": "2022-03"
      }
    ]
  ]
}
Enter fullscreen mode Exit fullscreen mode

Escreva um arquivo de teste com qualquer um dos exemplos, ou então crie seu próprio, como esse:

@annotation
class MyClass {
  @property accessor bool = false
}

function annotation(...params) {
  console.log(params)
}

function property(target, name) {
  console.log(target, name)
  return {
    get() {
      console.log('get')
      return target.get.call(this)
    },
    set(val) {
      console.log('set', val)
      return target.set.call(this, val)
    }
  }
}

function debug(target, { kind, name }) {
  if (kind === 'accessor') {
    const { get, set } = target
    return {
      get() {
        console.log(`get ${name}`)
        return get.call(this)
      },
      set(val) {
        console.log(`set ${name} para ${val}`)
        return set.call(this, val)
      },
      init(initialValue) {
        console.log(`iniciando ${name} com o valor ${initialValue}`)
        return initialValue
      }
    }
  }
}

const a = new MyClass()
console.log(a.bool)
a.bool = true
console.log(a.bool)
Enter fullscreen mode Exit fullscreen mode

Rode npm run transpile e depois node dist/<arquivo>.js e veja a mágica acontecer!

Conclusão

Os decorators são um padrão de projeto incrível e possuem um potencial imenso para serem uma das funcionalidades mais interessantes da linguagem e permitirem que façamos muito mais coisas de forma muito simples.

Eu, particularmente, vejo uma grande adoção por ferramentas de monitoramento como NewRelic, NSolid e Datadog para Node.js e até mesmo o JavaScript no browser!

Comenta ai embaixo o que você achou dessa proposta e me marca lá no Twitter pra eu saber sua opinião!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .