-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Decorator cookbook #231
Comments
|
More examples welcome! |
|
To generalize @b-strauss , wrapping a method, or wrapping a field initializer. |
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... |
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. |
|
|
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. |
|
Mixins! We migrated our codebase from an old pre ES6 inheritance pattern that looked like this:
to this:
to snaz up our mixin pattern we used mix / with:
but we did consider (and I would have preferred) decorators:
|
I would use decorators for Inversify, Tsoa and Nest.js!😀 |
In this library you can see many examples of decorators used with asynchronous functions. https://github.com/sithmel/async-deco |
@ljharb To clarify #231 (comment) , what do you mean exactly by a field-bound instance method? And what is the motivation for using this? |
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 |
|
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(); |
decorate a class method, do some parameter manipulation, such as injection, want to use a parameter decorator, sad for not supported now |
(Is it ok to provide the example use cases of a specific library?) 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 |
I very much encourage giving practical examples from specific libraries! If you can explain why this is useful for you, even better. |
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); |
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 |
A very complete collection of method decoration is https://github.com/steelsojka/lodash-decorators This library is a Decorators version of Lodash functions. |
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 Interesting, can you say more about what you use AssemblyScript decorators for? |
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) |
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 microsoft/TypeScript#4881 . |
In VUE ecosystem we can found:
They are an example about Vue components, Vuex and decorators. |
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:
|
I tried the babel plugin, but I could not get decorators for top level functions to work. 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. @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 you can get reasonably far with a const MyComponent = props => (
<div>
<Button kind="primary" onClick={() => console.log('clicked!')}>
Hello World!
</Button>
</div>
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
memo,
withTranslation,
withStyles(styles, { name: 'button' }),
)(MyComponent); That said, I agree with your point, and would definitely like to see function-level decorators as well. I think it's important for consistency (e.g. in a React context you'd want to decorate class and function components the same way), and I think things like |
Support for
There are others, but that depends on whether or not decorators will be allowed to inject new private names into the class's private name scope. |
+1 for the React examples, this is our most used use case so far. I would also like to use them for Lambda Middleware, looking like this: function Authenticate (target, propertyType, descriptor) {
const originalHandler = descriptor.value
descriptor.value = (event) => {
const userName = getUserNameFromAuthorizationHeader(event.headers.Authorization)
return originalHandler({...event, auth: { userName }})
}
return descriptor
}
class Controller {
@Authenticate
public greetUser (event) {
return {
status: 200,
body: `Hello ${event.auth.userName}`
}
}
}
new Controller.greetUser({
headers: {
Authorization: 'Bearer TOKEN'
}
}) I don't know of any library implementing this yet for lambdas, though, but might work on one in the near future. There are multiple libraries with this goal for other server frameworks like express, though. |
and a lot more.... |
Decorator-based aspect-oriented programming (AOP). See https://www.npmjs.com/package/@scispike/aspectify & https://github.com/SciSpike/aspectify. |
Some Node.js modules like TypeORM or routing-controllers use decorators to define and collect metadata of classes, methods and properties, similar to @interface annotation in Java. @Entity()
export class User {
@PrimaryColumn()
id: number;
@Column()
name: string;
} @JsonController()
export class UserController {
@Get("/users")
getAll() {
return userRepository.findAll();
}
} |
Decorators are used in vuex-class-modules to define vuex modules as typescript classes. Example from the readme: import { VuexModule, Module, Mutation, Action } from "vuex-class-modules";
@Module
class UserModule extends VuexModule {
// state
firstName = "Foo"
lastName = "Bar"
// getters
get fullName() {
return this.firstName + " " + this.lastName
}
// mutations
@Mutation
setFirstName(firstName: string) {
this.firstName = firstName
}
@Mutation
setLastName(lastName: string) {
this.lastName = lastName
}
// actions
@Action
async loadUser() {
const user = await fetchUser()
this.setFirstName(user.firstName)
this.setLastName(user.lastName)
}
} This uses a two step process of defining metadata via method decorators and then creating a proxy class with the |
I use decorators for a kind of dependency injection. The decorator is build like this: function inject(type: symbol, container: Container, args: symbol[]) {
return function(target: object, property: string): void {
Object.defineProperty(target, property, {
get: function() {
const value = container.get<any>(type);
if (args.indexOf(NOCACHE) === -1) {
Object.defineProperty(this, property, {
value,
enumerable: true,
});
}
return value;
},
configurable: true,
enumerable: true,
});
};
}
export function createDecorator(container: Container) {
return function(type: symbol, ...args: symbol[]) {
return inject(type, container, args);
};
} The decorator is used like this: class Example {
@inject(symbol)
readonly service!: Interface;
} Source: https://github.com/owja/ioc/blob/master/src/ioc/inject.ts |
These use cases are great! Keep them coming! If someone wants to try to write up these decorators in terms of the new decorators proposal, I would be very grateful. |
One of my main use cases is reactive properties ( The only way I can think of so far to intercept class fields (f.e. to make them "reactive" which involves needing to make a getters and setters for them), without using Something like this (EDIT: the following is broken, not working with more than one class, see fixed version below): const propsToSignalify = new Map()
export function signal(...args) {
const [_, {kind, name}] = args
if (kind !== 'field') new TypeError('not field')
return function (initialValue) {
propsToSignalify.set(name, {name, initialValue})
return initialValue
}
}
export function reactive(...args) {
const [value, {kind, name}] = args
if (kind !== 'class') throw new TypeError('not class')
return class Reactive extends value {
constructor(...args) {
super(...args)
for (const [name, {initialValue}] of propsToSignalify) {
// This replaces the class field descriptor with a new one.
makePropertyReactiveWithGetterSetter(this, name, initialValue)
}
propsToSignalify.clear()
}
}
} import {reactive, signal, effect} from 'reactive-lib'
@reactive
class Foo {
@signal foo = 123
}
@reactive
class Bar extends Foo {
@signal bar = 456
}
const b = new Bar
setInterval(() => {
b.foo++
b.bar++
})
// Each of these logs re-run any time b.foo or b.bar change.
effect(() => console.log(b.foo))
effect(() => console.log(b.bar)) where imagination has to be used for the The main problem with this approach is that the user must not forget to use In previous decorators spec, a class finisher along with access to the class in field decorators prevented the need for the extra Is there a better way? Here's what it looks like with several properties in one class: @reactive
class Ipsum extends Lorem {
@signal foo = 456
@signal bar = 456
@signal baz = 456
} Using // ... re-make function reactive to work on accessor fields...
class Ipsum extends Lorem {
@signal accessor foo = 456
@signal accessor bar = 456
@signal accessor baz = 456
} But apart from being human-forgettable, I like the class decorator more than Or maybe "signal accessor" is actually more semantic in this particular naming scheme, because the properties "access signals" underneath. I still think I like the more concise one with class decoration (it makes a difference with even more props). |
@trusktr there is not another way, and this is by design. Using |
Nvm, my approach above totally fell apart with more than one class (brain fart, I thought the Reactive subclass happened at class define time, like a class finisher, doh). Hmmm. |
It looks like this is no longer possible with Stage 3 decorators. Have you given it a try yet? |
Turns out it is possible with a little dancing🕺. Here's my previous example fixed, with helpful errors in case the class decorator is forgotten: const propsToSignalify = new Map()
const classFinishers = []
export function signal(...args) {
const [_, {kind, name}] = args
let props = propsToSignalify
classFinishers.push(propsToSignalify => (props = propsToSignalify))
if (kind !== 'field') new TypeError('not field')
props.set(name, {initialValue: undefined})
return function (initialValue) {
props.get(name)!.initialValue = initialValue
return initialValue
}
queueReactiveDecoratorChecker()
}
const hasOwnProperty = Object.prototype.hasOwnProperty
export function reactive(...args) {
const [value, {kind, name}] = args
if (kind !== 'class') throw new TypeError('not class')
const props = new Map(propsToSignalify)
propsToSignalify.clear()
for (const finisher of classFinishers) finisher(props)
classFinishers.length = 0
return class Reactive extends value {
constructor(...args) {
super(...args)
for (const [name, {initialValue}] of propsToSignalify) {
if (!(hasOwnProperty.call(this, prop) || hasOwnProperty.call((value as Constructor).prototype, prop))) {
throw new Error(
`Property "${prop.toString()}" not found on object. Did you forget to use the \`@reactive\` decorator on a class that has properties decorated with \`@signal\`?`,
)
}
// This replaces the class field descriptor with a new one using a Solid signal.
makePropertyReactive(this, name, initialValue)
}
}
}
}
let checkerQueued = false
function queueReactiveDecoratorChecker() {
if (checkerQueued) return
checkerQueued = true
queueMicrotask(() => {
checkerQueued = false
if (propsToSignalify.size) {
throw new Error(
`Stray @signal-decorated properties detected: ${[...propsToSignalify.keys()].join(
', ',
)}. Did you forget to use the \`@reactive\` decorator on a class that has properties decorated with \`@signal\`?`,
)
}
})
} And we can further add support for Usage: @reactive
class Foo {
@signal foo = 123
}
// oops, no @reactive, will throw an error
class Bar extends Foo {
@signal bar = 456
} This sort of decoration will cost the extra descriptor defines, and is maybe only ever-so-slightly slower than a pattern like: class Foo {
foo = 1
bar = 2
constructor() {
signalify(this, 'foo', 'bar')
// or signalify(this) // signalify all properties
}
} |
To clarify, I meant that I would avoid the pattern you have here of defining getters and setters on the instance. This was actually considered as possible behavior for class field decorators, but the issue is that accessors on the instance can never be inlined, that can only happen to accessors defined on a prototype. At scale, this will result in significantly worse performance compared to using |
I think it will be fine though, the performance may be worse, but in most cases not human-time perceivable. I'm using this pattern with 3D rendering and it has been fine (typically not creating objects over and over, but keeping a set of objects alive over time and toggling features like visibility instead, etc). I think this, @reactive class Foo {
@signal foo = 1
@signal bar = 1
@signal baz = 1
@signal lorem = 1
@signal ipsum = 1
} is cleaner than class Foo {
@signal accessor foo = 1
@signal accessor bar = 1
@signal accessor baz = 1
@signal accessor lorem = 1
@signal accessor ipsum = 1
} because the I mean, yeah, implementation wise, for someone who knows how the implementation of the decorators work, the |
I think it's probably bad to train users that this works and confuse them as to when they need to use |
I have been able to shave multiple seconds off of initial render time with these types of optimizations in complex applications at scale. For small apps it likely will not have a major impact, but anything beyond a trivial size will benefit significantly from inlining. |
It is not necessarily bad: the lib's documentation is responsible for telling users how to use the decorators. If the lib has bad docs, then yeah. |
It would be interesting to see an example. You must be talking about a big app with tons of decorators. |
The app was linkedin.com. It was written in Ember.js, which was undergoing a rewrite to its main rendering engine, Glimmer. The rewrite was initially a significant regression, but we were able to optimize until it was about the same as the original. The single biggest optimization we did was monomorphising core classes (e.g. taking an interface that was implemented as many classes and turning it into a single class with a shape that was predictable to the engine). Monomorphic classes are very useful to the engine because it allows them to inline calls to methods on those classes, which means calling the method requires far less CPU time. This optimization saved several seconds on the initial render of linkedin.com (p90), according to our benchmarks. Relevant PR with benchmarks for a much smaller and significantly less complicated opensource app (emberobserver.com): emberjs/ember.js#18223 When an getter/setter is defined directly on an instance of a class, rather than the prototype, it can never benefit from this type of optimization. This was a key discussion point in designs for decorators. At one point, I suggested that field decorators could intercept the |
I'm having a hard time now that class field decorators (and field access decorators) don't have access to the class reference anymore. I used to keep track of the "index" of each decorated field on a custom serializer, e.g.: export class Player extends Schema {
@type("string") name: string; // index 0
@type("number") x: number; // index 1
@type("number") y: number; // index 2
} Using legacy decorators, it was possible to keep track of field indexes by doing this: (this is a simplification of what I have on colyseus/schema for demonstration) export function type (type) {
return function (target, field) {
const ctor = target.constructor;
if (!ctor._definition) {
ctor._definition = {
fields: [],
descriptors: {},
};
}
ctor._fields.push(field);
const fieldCached = `_${field}`;
// define property descriptors (they're attached at the `constructor` of the base Schema class)
ctor._definition.descriptors[field] = {
get: function () { return this[fieldCached]; },
set: function (value) {
const index = ctor._fields.indexOf(field)
// perform action with "index"
this[fieldCached] = value;
},
enumerable: true,
configurable: true
};
}
} This might not be the right place to look for guidance, but I'd appreciate it if any of you could suggest an approach using the new decorators API. |
@endel Maybe you could store this counter in the metadata? |
Thank you for your swift response @littledan. Indeed |
A use case I've been working on is extending the Node.js EventEmitter with decorators. The first being I've run into some interesting challenges with this. For instance, I thought it would be cool to allow something like I think if I tried an I also made a simpler This highlights a perhaps counter-intuitive fact about decorators: When calling a decorator factory function like these, you do not have access to the class you are decorating. You can't do |
Hello. Another interesting use for decorators involves providing lambda expressions as arguments to decorators such that these lambda expressions in source code could, additionally, be processed by type-checking, constraint-logic, or other reasoning engines to further convenience software development. I very recently broached these topics here: microsoft/TypeScript#60093 . Also, a repository is available for these ideas. Here is an updated example: @constraint((obj: Foo) => { assert(obj.x > obj.y) })
@constraint((obj: Foo) => { assert(obj.y >= 0) })
@constraint((obj: Foo) => { assert(obj.x >= 0) })
class Foo
{
x: number;
y: number;
}
@constraint((obj: ExtendsFoo) => { assert(obj.z >= 0) })
class ExtendsFoo extends Foo
{
z: number;
} |
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:
The text was updated successfully, but these errors were encountered: