This package attempts to improve the way classes are decorated (see: decorator proposal) by not polluting the prototype chain, encouraging composition over inheritance.
Install
This package requires @babel/plugin-proposal-decorators
.
$ npm i -D @babel/plugin-proposal-decorators
$ npm i @darkobits/class-decorator
Then, update your .babelrc
file:
{
"plugins": [
["@babel/plugin-proposal-decorators", {
"legacy": true
}]
]
}
Use
ClassDecorator
This package's default export is a function that accepts a decorator implementation function and returns a decorator that may be applied to a class. The decorator implementation function is invoked with the target class. If the decorator implementation returns
a function, that function will be used as a proxy constructor for the decorated class. The proxy constructor will be passed an object with the following shape:
{
constructor: Function;
args: Array<any>;
}
Example:
In this example, we will create a higher-order decorator that accepts a list of superpowers to apply to class instances.
First, we will look at how this is done using the typical approach, then how to accomplish it using this package.
function AddSuperpowers (...powers) {
return function (Ctor: Function): Function {
return class AddSuperPowers extends Ctor {
constructor(...args) {
super(...args);
powers.forEach(power => {
this[power] = true;
});
}
hasSuperpower(power: string): boolean {
return this[power];
}
}
};
}
@AddSuperpowers('strength', 'speed', 'flight')
class Person {
constructor(name) {
this.name = name;
}
getName(): string {
return this.name;
}
}
const bob = new Person('Bob');
bob.strength;
bob.speed;
bob.flight;
This is great, but if we examine the prototype chain of the bob
instance, it will look something like this:
bob: {
name: 'Bob'
strength: true
speed: true
flight: true
[[Prototype]] => AddSuperpowers: {
} hasSuperpower()
[[Prototype]] => Person: {
} getName()
[[Prototype]] => Object
}
If we used 5 decorators on the Person
class, we would find 5 degrees of inheritance added to each instance of Person
. Decorators should faciliate composition, not exacerbate existing issues with inheritance. You might also notice that our decorator's prototype inherits from our original class, meaning that consumers of our decorator will not be able to shadow properties or methods applied by the decorator. This is bad behavior; the decorated class should always retain the ability to shadow properties set by ancestors and decorators.
Let's see how with a few modifications we can improve this situation:
import ClassDecorator from '@darkobits/class-decorator';
const AddSuperpowers = (...powers: Array<any>): Function => ClassDecorator(Ctor => {
Ctor.prototype.hasSuperpower = function (power: string): boolean {
return this[power];
};
return function ({constructor, args:}: {constructor: Function; args: Array<any>}): void {
powers.forEach(power => {
this[power] = true;
});
constructor(...args);
}
});
@AddSuperpowers('strength', 'speed', 'flight')
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
}
const bob = new Person('Bob');
If we looked at the protoype chain for this instance of bob
, we would see:
bob: {
name: 'Bob'
strength: true
speed: true
flight: true
[[Prototype]] => Person: {
} getName()
hasSuperpower()
[[Prototype]] => Object
}
MethodDecorator
Accepts a decorator implementation function and returns a decorator that may be applied to class methods. The decorator implementation function is invoked each time the decorated method is invoked, and is provided an object with the following shape:
{
method: Function;
methodName: string;
args: Array<any>;
}
Example:
import {MethodDecorator} from '@darkobits/class-decorator';
const AddSalutation = MethodDecorator(({method}) => {
return `Hello, my name is ${method()}.`;
});
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@AddSalutation
getName(): string {
return this.name;
}
}
const bob = new Person('Bob');
bob.getName()