proposal-decorators icon indicating copy to clipboard operation
proposal-decorators copied to clipboard

Decorator cookbook

Open littledan opened this issue 7 years ago • 60 comments

Please post your use cases for decorators as an issue comment, and we'll see if we can document them in the cookbook

Let's write a how-to guide about how to achieve various things with decorators, including:

  • A class decorator to "register" a class once it's created
  • A field decorator to create an accessor and underlying field
  • ...

littledan avatar Jan 22 '19 21:01 littledan

  • Making a private field conditionally public based on NODE_ENV, for testing
  • Making a public field with an arrow function into a field-bound instance method
  • Similarly, field-binding instance methods
  • making a method throw when it's passed the wrong number of arguments, or the wrong type of arguments

ljharb avatar Jan 22 '19 21:01 ljharb

More examples welcome!

littledan avatar Jan 22 '19 21:01 littledan

  • A method that logs all its parameters on execution
  • A method that is throttled via requestAnimationFrame
  • A method that caches its result based on primitive param values

b-strauss avatar Jan 23 '19 10:01 b-strauss

To generalize @b-strauss , wrapping a method, or wrapping a field initializer.

littledan avatar Jan 23 '19 11:01 littledan

I generally use them for argument validation or permissions and also, response/return sanitization. Not sure if it would help but I’m sure everyone does this...

rgolea avatar Jan 23 '19 13:01 rgolea

Documentation/annotation. Not all use cases need to be stuff that makes it all the way to runtime, I think — even if stripped during a build step for prod, it’s an improvement over comment-based doc generation because they’ll be first-class AST nodes associated with the correct context.

bathos-wistia avatar Jan 23 '19 13:01 bathos-wistia

  • Extending method in ClassDecorator that could be defined by user or exist on prototype chain. If it is not defined at all, fallback can be used.
  • Viewing private names added by a ClassDecorator in a coupled PropertyDecorator.

Lodin avatar Jan 23 '19 13:01 Lodin

  • async methods that return custom thenables
  • more generally, generator methods with custom “drivers”
  • or generator methods that implement custom prototypes (awkward currently, but can be a useful part of the generator model)
  • branding
  • web idl mixins — e.g. @maplike implementing the common methods
  • emitting one-time deprecation notices on access or invocation
  • declarative async dep graphs “for free” if you combine async fns + a @once caching decorator. this is hard to explain w/o an illustration. I did something like this a few years back in the context of an angular 1 app that made a lot of interrelated API calls using the OG decorators proposal and I think it can be a very useful pattern:
    class Foo {
      @once
      async getCats() {
        return api.getCats();
      }
    
      @once
      async getDogs() {
        return api.getDogs();
      }
    
      @once
      async getPetOwners() {
        const [ cats, dogs ] = await Promise.all([ this.getCats(), this.getDogs() ]);
        const ownerIDs = new Set([ ...cats, ...dogs ].flatMap(pet => pet.ownerIDs));
        return api.getPeopleByIDs(ownerIDs);
      }
    
      @once
      async getQuadrupedCount() {
        const [ cats, dogs ] = await Promise.all([ this.getCats(), this.getDogs() ]);
        return cats.length + dogs.length;
      }
    
      @once
      async getResources() {
        const [ cats, dogs, petOwners, quadupedCount ] = await Promise.all([
          this.getCats(),
          this.getDogs(),
          this.getPetOwners(),
          this.getQuadrupedCount()
        ]);
    
        return { cats, dogs, petOwners, quadupedCount };
      }
    }
    
    // maybe sometimes you need all resources, sometimes you just need the quad count, etc.
    // no matter what you request, though, you’re only making the minimum number of api calls
    // per instance.
    

bathos-wistia avatar Jan 23 '19 14:01 bathos-wistia

Route Authentication

Wrap a route with a function that verifies that the user is authenticated with the correct role, reject the request with the proper HTTP response if otherwise.

I have implemented Basic-Auth using this approach in Python. It made auth soooo much easier to apply authentication to routes on a case-by-case basis.

Debugging: Count # of executions

Add a stateful counter decorator that increments every time a function is called and logs it to the console. Can be useful for tracking down obscure bugs.

Debugging: Measure execution time

Add a timer method that marks the time before, after, calculates the difference and logs it to the console. Useful for pinpointing hotspots in the code for optimization.

Apologies, the sample is in Typescript.

export function Timer(target: object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) { 
  const originalMethod = descriptor.value; 
 
  descriptor.value = function(...args: any[]) { 
    const start = performance.now(); 
    const result = originalMethod.apply(this, args); 
    const stop = performance.now(); 
    const elapsed = (stop - start).toFixed(2); 
    console.info(`%c[Timer]%c`, `color: blue`, `color: black`, `${target.constructor.name}.${propertyKey}() - ${elapsed} milliseconds`); 
 
    return result; 
  }; 
 
  return descriptor; 
}

Augmenting Classes

Sometimes it's useful to augment classes with additional behavior without adding a ton of boilerplate. For example, on the last big project I worked on we used custom scrollbars on a significant number of UI elements that were provided by an external lib.

To use the scrollbar required adding an instance prop to the class, and attaching it to the inner HTMLElement during construction. It could also be configured to use either fat/thin variants.

This was very easy to accomplish using a ClassDecorator. Unfortunately, we didn't use it because Typescript's type system couldn't see props that get dynamically added to a class during runtime. The only way to make it work was to pre-define the prop on the class.

Logging class/method/function Params

Somebody already mentioned this but you could write a function that collects all of the params attached to a class/method/function and logs them to the console and/or attach an event that can do the logging when it's fired.

evanplaice avatar Jan 23 '19 18:01 evanplaice

  • Writing metadata to later generate documentation: for example for an OpenAPI schema.

  • Changing a class definition at runtime / injecting arguments into its constructor:

    @ inject example

    function inject(...providers) {
      return function (classDescriptor) {
        return {
          ...classDescriptor,
          finisher(klass) {
            class Injectable {
              constructor(...args) {
                return Reflect.construct(klass, [...container.getAll(providers), ...args], Injectable);
              }
            }
            Reflect.setPrototypeOf(Injectable, klass);
            Reflect.setPrototypeOf(Injectable.prototype, klass.prototype);
            return Injectable;
          }
        }
      }
    }
    

isidrok avatar Jan 23 '19 19:01 isidrok

Mixins!

We migrated our codebase from an old pre ES6 inheritance pattern that looked like this:

const MySpectacularPage = BasePage.extend(SparklesMixin, SprinklesMixin, {});

to this:

class MySpectacularPage extends SprinklesMixin(SparklesMixin(BasePage)) {}

to snaz up our mixin pattern we used mix / with:

class MySpectacularPage extends mix(BasePage).with(SparklesMixin, SprinklesMixin) {}

but we did consider (and I would have preferred) decorators:

@SparklesMixin
@SprinklesMixin
class MySpectacularPage extends BasePage {}

goodforenergy avatar Jan 23 '19 20:01 goodforenergy

I would use decorators for Inversify, Tsoa and Nest.js!😀

gitowiec avatar Jan 23 '19 20:01 gitowiec

In this library you can see many examples of decorators used with asynchronous functions. https://github.com/sithmel/async-deco

sithmel avatar Jan 24 '19 07:01 sithmel

@ljharb To clarify https://github.com/tc39/proposal-decorators/issues/231#issuecomment-456574719 , what do you mean exactly by a field-bound instance method? And what is the motivation for using this?

littledan avatar Feb 01 '19 13:02 littledan

foo = foo.bind(this);
foo() { }

The motivation is that arrows in class fields make it very hard to test react components, because you can’t mock/stub foo prior to instantiating the class (when testing a react component, one generally creates the element and passes it into enzyme in one swoop, which doesnt give you an opportunity to intercept the instance before references to the instance arrow functions are passed into the render tree). By converting to a field-bound instance method, tests can spy on the prototype prior to creating the instance. This problem is the source of dozens of bugs filed on enzyme, and the reason the advice i give is to never put functions in class fields. This decorator could serve as an alternative for those who find the syntax sugar more compelling than being able to test their code.

ljharb avatar Feb 01 '19 16:02 ljharb

  • class decorators for adding metadata to a class (somewhat like static fields but more private)
  • property decorators that can read/mutate the above metadata
  • property decorators that can gather/inspect runtime artefacts. (as a sample, a litElement can have internal HTML-CSS that is changed at runtime, sometimes you want to extract things there) (this one will probably slow path) and expose those as a property.

SanderElias avatar Feb 12 '19 14:02 SanderElias

You can use decorators for define other decorators. It's is posible by a simple replace hook than return a function, and it's very useful because you can define an class extensions as other class and use this extensions as a decorator.

@Decorator
class Onoff {

  @Override
  on() {
    /... 
  }
  
  @Override 
  off() { 
    /... 
  }
}

@Onoff  // This decorator is defined as a class (specially decorated) 
class MyClass {
}

const m = new MyClass();
m.on();

pabloalmunia avatar Feb 15 '19 19:02 pabloalmunia

decorate a class method, do some parameter manipulation, such as injection, want to use a parameter decorator, sad for not supported now

czewail avatar Feb 16 '19 10:02 czewail

(Is it ok to provide the example use cases of a specific library?) In capsid.js, you can define (DOM) event listeners by decorators, and can make a field into a query selector by decorators:

const { on, wired, component } = require("capsid");

@component("mirroring") // This registeres the class as a capsid component
class Mirroring {
  @wired(".dest") // This decorator makes the field equivalent of `get dest () { return this.el.querySelector('.dest'); }`
  dest;
  @wired(".src")
  src;

  @on("input") // This decorator makes the method into an event listener on the mounted element. In this case, onReceiveData is bound to `input` event of the mounted element
  onReceiveData() {
    this.dest.textContent = this.src.value;
  }
}

Here is the working demo at codesandbox

kt3k avatar Feb 16 '19 12:02 kt3k

I very much encourage giving practical examples from specific libraries! If you can explain why this is useful for you, even better.

littledan avatar Feb 16 '19 13:02 littledan

Exist a group of constructor's intersection patterns very useful. For example, with a simple @Singleton decorator we can define an object shared between all class users.

function Singleton (descriptor) {
  return {
    ...descriptor,
    finisher (Cls) {
      let ref;
      return function () {
        if (ref) return ref;
        return ref = new Cls ();
      }
    }
  }
}

@Singleton
class M {
}

const m1 = new M();
const m2 = new M();
console.assert(m1 === m2);

pabloalmunia avatar Feb 16 '19 19:02 pabloalmunia

Decorators and Proxies together are an extreme powerful combination. For example, we can define a method as Default with a decorator and rewrite the constructor for return a proxy. As a result, if we call an unknow member, the default method is called with this value.

function Default (descriptor) {
  return {
    ...descriptor,
    finisher (Cls) {
      return function (...args) {
        const result = new Cls (...args);
        return new Proxy (result, {
          get (target, prop, receiver) {
            if (prop in target) {
              return Reflect.get (target, prop, receiver);
            }
            const origin = Reflect.get (target, descriptor.key, receiver);
            return origin (prop);
          }
        });
      };
    }
  };
}

class Database {
  open  () {}
  close () {}
  
  @Default
  table (name) {
    return {
      find   () {},
      add    () {},
      remove () {},
      edit   () {}
    }
  }
}

const db = new Database ();
db.table ('users').find ();
db.users.find ();  // .users isn't a member, but the default method is called with this value

pabloalmunia avatar Feb 16 '19 19:02 pabloalmunia

A very complete collection of method decoration is https://github.com/steelsojka/lodash-decorators

This library is a Decorators version of Lodash functions.

pabloalmunia avatar Feb 17 '19 09:02 pabloalmunia

AssemblyScript which superset of JavaScript using function level decorators a lot, but only for built-in decorators so no hoisting problems. Is it possible use function level decorator with this proposal for non-runtime decorators in future?

MaxGraey avatar Feb 17 '19 15:02 MaxGraey

@MaxGraey Interesting, can you say more about what you use AssemblyScript decorators for?

littledan avatar Feb 17 '19 15:02 littledan

Ok couple useful examples:

/* [built-in] hint for compiler which should always inline this func. 
 * Simpilar to `inline` in other langs
 */
@inline
function toDeg(x: number): number {
   return x * (180 / Math.PI);
}

example below just proof of concept. Not supported yet by AS:

/* [built-in] similar to `constexpr` in C++. 
 * Force expression evaluation during compilation. Valid only for constant arguments.
 */
@precompute // or @const ?
function fib(x: number): number {
   if (n <= 1) return n;
   return fib(n - 1) + fib(n - 2);
}
/* [built-in] indicate that function pure and could be potentially optimized for 
 * high ordered functions and lambda calculus
 */
@pure
function action(type: string, payload: object): object {
   return {
      type,
      ...payload
   };
}
/* [built-in] 
 * Same hint for TCO like in Kotlin
 */
@tailrec
function findFixPoint(x = 1.0): number {
  return Math.abs(x - Math.cos(x)) < eps ? x : findFixPoint(Math.cos(x));
}

Javascript world more and more shifted to functional paradigm and in this case functional level decorators is very necessary in my opinion)

MaxGraey avatar Feb 17 '19 15:02 MaxGraey

Decorators would be great for applying React higher-order components which inject React props. Here's a JSX example of what I was trying to get working to inject a prop from a React Context.

import React from 'react';
import update from "immutability-helper";

const userCtxDefaults = {name: 'J Doe'};
const UserContext = React.createContext(userCtxDefaults);

/**
 * Injects the userCtx prop from UserContext
 */
function withUserContext(Component) {
  return class extends React.Component {
    static displayName = `withUserContext(${Component.displayName || Component.name})`;

    render() {
      return (
        <UserContext.Consumer>
          {userCtx => {
            const props = update(this.props, {userCtx: {$set: userCtx}});
            return <Component {...props} />;
          }}
        </UserContext.Consumer>
      );
    }
  };
}

@withUserContext
class MyComponent extends React.Component {
  render() {
    return `name: ${this.props.userCtx.name}`
  }
}

It doesn't work in Typescript yet because they're waiting on this to reach stage 3 or 4 https://github.com/Microsoft/TypeScript/issues/4881 .

mikepii avatar Feb 18 '19 21:02 mikepii

In VUE ecosystem we can found:

  • https://github.com/vuejs/vue-class-component
  • https://www.npmjs.com/package/vue-property-decorator
  • https://github.com/ktsn/vuex-class/

They are an example about Vue components, Vuex and decorators.

pabloalmunia avatar Feb 19 '19 21:02 pabloalmunia

Made an overview of the most important use cases in MobX including some comments. (Sorry, noticed there was this issue for it only afterwards, I can bring the stuff here if needed). https://gist.github.com/mweststrate/8a4d12db0e11ca536c9ff3b6ba754243

Abstract summary:

  1. manipulate a constructor / prototype (no problems in stage 0)
  2. transform a descriptor (e.g. wrap a function) on the prototype (no problems in stage 0)
  3. transform a value based descriptor to getter / setter on the prototype (no problems in stage 0)
  4. transform a value based descriptor + field initializer into a descriptor on the instance. Lot's of trouble here, mostly: initializer cannot be run before the first access to the field (on the prototype). Which makes the field go missing in reflection. Can be fixed by either [[set]] for fields (e.g. TS doesn't give an problems here) or, by running the decorators for fields with initializer as part of the constructor (so that fields can be created eagerly)

mweststrate avatar Feb 21 '19 13:02 mweststrate

I tried the babel plugin, but I could not get decorators for top level functions to work. Is this proposal not about top level functions? If so, I would love to have them added to this proposal, or that there would come another proposal that covers decorators for top level functions.

In the React ecosystem there would be a lot of usecases:

@connect(mapStateToProps, mapDispatchToProps)
@memo
@withTranslation 
@withStyles(styles, { name: 'button' })
export const MyComponent = props => (
  <div>
    <Button kind="primary" onClick={() => console.log('clicked!')}>
      Hello World!
    </Button>
  </div>
);

Without decorators, you will end up with code like this:

let MyComponent = props => (
  <div>
    <Button kind="primary" onClick={() => console.log('clicked!')}>
      Hello World!
    </Button>
  </div>
);
MyComponent = connect(
  mapStateToProps,
  mapDispatchToProps,
)(MyComponent);
MyComponent = memo(MyComponent);
MyComponent = withStyles(styles, { name: 'button' });
export { MyComponent };

Or you could write like this:

export const MyComponent = withStyles(styles, { name: 'button' })(
  memo(
    connect(
      mapStateToProps,
      mapDispatchToProps,
    )(props => (
      <div>
        <Button kind="primary" onClick={() => console.log('clicked!')}>
          Hello World!
        </Button>
      </div>
    )),
  ),
);

Of course there are many other possible usecases, Iike @MaxGraey examples. I would probably use decorators like this:

@curry // curry all arguments so that they can be partially applied
export const every = (predicate, iterable) => {
  for (const item of iterable) {
    if (!predicate(item)) return false;
  }
  return true;
};
@memoize // caches the function results in a ES2015 Map as the function is pure 
export const isPrime = p =>
  pipe(
    naturals(2),
    takeWhile(n => n ** 2 <= p),
    every(n => p % n !== 0),
  );

kasperpeulen avatar Feb 22 '19 08:02 kasperpeulen