Programmatically reusable decorators for TypeScript
import { AeMember } from '@proc7ts/amend';
class MyClass {
@AeMember(({ key, get, set, amend }) =>
amend({
get(instance) {
// Replace the getter.
const value = get(instance); // Read the value with default getter.
console.debug(`${key} value read:`, value);
return value;
},
set(instance, update) {
// Replace the setter.
const oldValue = get(instance);
set(instance, update);
console.debug(`${key} value updated:`, oldValue, ' -> ', update);
},
}),
)
field = 'value';
}
Here @AeMember() creates an amendment that can be used as a class member decorator. This decorator can be
applied to property, accessor, or method. In any case the provided get
and set
functions read and write values
correspondingly.
The @AeMember() accepts arbitrary number of nested amendments. Each amendment receives an object with the following properties (from AeMember and AmendTarget.Core interfaces):
amendedClass
- Amended class constructor.key
- Amended member key.configurable
- Whether the amended member is configurable.enumberable
- Whether the amended member is enumerable.readable
- Whether the amended member is readable. The member is readable, unless it has only setter.writable
- Whether the amended member is writable. The member is writable, unless it has only setter, or it is defined non-writable.get(instance)
- Member value reader function.set(instance, update)
- Member value writer function.amend(request)
- Member amendment function.
The amend() function call modifies the member definition. It accepts an object with the same properties and overrides the member definition:
-
If
get
orset
specified and differ from the passed in value, then the member converted to accessor with correspondingget
and/orset
operations.If
get
omitted, then the member becomes non-readable.If
set
omitted, then the member becomes non-writable.Note that
get
andset
operations passed in still could be used even from inside their replacements. They would act as before. -
If neither
get
, norset
specified, then the member value access operations remain unchanged. -
If
configurable
orenumerable
specified and differ from the values passes in, then these properties used to update the property descriptor. -
The rest of the properties are ignored.
The @AeStatic() creates an amendment that can be used as a static class member decorator. It is equivalent to @AeMember(), except it is applicable to static members.
The @AeClass() creates an amendment that can be used as a class decorator.
The nested amendments receive an object with only amendedClass
and amend()
properties.
Custom amendment can be created by function that calls one of the predefined ones. It can be declared like this:
import { AeMember, AmendTarget, MemberAmendment } from '@proc7ts/amend';
import { Class } from '@proc7ts/primitives';
export function LoggedMember<
TValue extends TUpdate, // Member value type.
TClass extends Class = Class, // Amended class type.
TUpdate = TValue, // Member value update type accepted by its setter.
TAmended extends AeMember<TValue, TClass, TAmended> = AeMember<TValue, TClass, TAmended>, // Amended entity type.
>(): MemberAmendment<TValue, TClass, TUpdate, TAmended> {
return AeMember(
(
{ key, get, set, amend }: AmendTarget<AeMember<TValue, TClass, TUpdate>>, // Amendment target. Contains amended entity properties
) =>
// along with `amend()` function.
amend({
get(instance) {
// Replace the getter.
const value = get(instance); // Read the value with default getter.
console.debug(`${key} value read:`, value);
return value;
},
set(instance, update) {
// Replace the setter.
const oldValue = get(instance);
set(instance, update);
console.debug(`${key} value updated:`, oldValue, ' -> ', update);
},
}),
);
}
Then the first example could be rewritten like this:
class MyClass {
@LoggedMember()
field = 'value';
}
The simplest way to combine multiple amendments is to apply multiple decorators.
However, it is possible to declare a combined amendment that applies multiple amendments by single decorator:
import { AeMember, AmendTarget, MemberAmendment } from '@proc7ts/amend';
import { Class } from '@proc7ts/primitives';
/**
* Logs member reads.
*/
export function ReadLoggedMember<
TValue extends TUpdate, // Member value type.
TClass extends Class = Class, // Amended class type.
TUpdate = TValue, // Member value update type accepted by its setter.
TAmended extends AeMember<TValue, TClass, TAmended> = AeMember<TValue, TClass, TAmended>, // Amended entity type.
>(): MemberAmendment<TValue, TClass, TUpdate, TAmended> {
return AeMember(({ key, get, set, amend }: AmendTarget<AeMember<TValue, TClass, TUpdate>>) =>
amend({
get(instance) {
// Replace the getter.
const value = get(instance); // Read the value with default getter.
console.debug(`${key} value read:`, value);
return value;
},
set, // The setter remains unchanged.
}),
);
}
/**
* Logs member writes.
*/
export function WriteLoggedMember<
TValue extends TUpdate, // Member value type.
TClass extends Class = Class, // Amended class type.
TUpdate = TValue, // Member value update type accepted by its setter.
TAmended extends AeMember<TValue, TClass, TAmended> = AeMember<TValue, TClass, TAmended>, // Amended entity type.
>(): MemberAmendment<TValue, TClass, TUpdate, TAmended> {
return AeMember(({ key, get, set, amend }: AmendTarget<AeMember<TValue, TClass, TUpdate>>) =>
amend({
get, // The getter remains unchanged.
set(instance, update) {
// Replace the setter.
const oldValue = get(instance);
set(instance, update);
console.debug(`${key} value updated:`, oldValue, ' -> ', update);
},
}),
);
}
/**
* Logs any member access.
*/
export function LoggedMember<
TValue extends TUpdate, // Member value type.
TClass extends Class = Class, // Amended class type.
TUpdate = TValue, // Member value update type accepted by its setter.
TAmended extends AeMember<TValue, TClass, TAmended> = AeMember<TValue, TClass, TAmended>, // Amended entity type.
>(): MemberAmendment<TValue, TClass, TUpdate, TAmended> {
// Apply both amendments in chain.
return AeMember(ReadLoggedMember(), WriteLoggedMember());
}
The library contains a few more helpful amendments:
- @AeMembers() - A class amendment that amends existing and declares new class members.
- @AeStatics() - A class amendment that amends existing and declares new static members.
- @PseudoMember() - A class amendment that declares a pseudo-member, which is not actually defined in class prototype. Such member value may be derived from the real one.
- @PseudoStatic() - A class amendment that declares a static pseudo-member, which is not actually defined in class constructor.
See the API documentation for the detailed info.
There are two issues with TypeScript decorators:
-
They are experimental. They may change in future releases, and probably will due to ECMAScript chosen another approach.
-
Each TypeScript decorator adds a
__decorate()
function call to generated JavaScript. This function has side effects, so the bundler is unable to tree-shake it. The latter could be a major issue (especially for the library authors), as a bundler would add all decorated classes to application bundle, even unused ones.
Auto-amendment designed to resolve these issues.
To make it work just extend an Amendable abstract class, and place the amendments to autoAmend static method:
import { AeClassTarget, AeMembers, Amendable } from '@proc7ts/amend';
class MyClass extends Amendable {
static autoAmend(target: AeClassTarget<typeof MyClass>): void {
// Apply amendments here.
AeMembers({
field: LoggedMember(), // An amendment of `field` property.
}).applyAmendment(target);
}
field = 'value';
}
Auto-amendment will be applied to the class when the first instance of that class constructed.
Alternatively, the class could be amended explicitly by calling an amend() function with that class as an argument. The class (and its super-classes) would be auto-amended at most once. The amend() method could be safely called multiple times for the same class.
It is not necessary to extend an Amendable class if amend() will be called explicitly for the class. It would be enough to implement an autoAmend static method.
An explicit amendment with amend() function call could be necessary, e.g. when accessing amended static members.