-
Star
(372)
You must be signed in to star a gist -
Fork
(90)
You must be signed in to fork a gist
-
-
Save remojansen/16c661a7afd68e22ac6e to your computer and use it in GitHub Desktop.
| function logClass(target: any) { | |
| // save a reference to the original constructor | |
| var original = target; | |
| // a utility function to generate instances of a class | |
| function construct(constructor, args) { | |
| var c : any = function () { | |
| return constructor.apply(this, args); | |
| } | |
| c.prototype = constructor.prototype; | |
| return new c(); | |
| } | |
| // the new constructor behaviour | |
| var f : any = function (...args) { | |
| console.log("New: " + original.name); | |
| return construct(original, args); | |
| } | |
| // copy prototype so intanceof operator still works | |
| f.prototype = original.prototype; | |
| // return new constructor (will override original) | |
| return f; | |
| } | |
| @logClass | |
| class Person { | |
| public name: string; | |
| public surname: string; | |
| constructor(name : string, surname : string) { | |
| this.name = name; | |
| this.surname = surname; | |
| } | |
| } | |
| var p = new Person("remo", "jansen"); |
| @logClassWithArgs({ when : { name : "remo"} }) | |
| class Person { | |
| public name: string; | |
| // ... | |
| } | |
| function logClassWithArgs(filter: Object) { | |
| return (target: Object) => { | |
| // implement class decorator here, the class decorator | |
| // will have access to the decorator arguments (filter) | |
| // because they are stored in a closure | |
| } | |
| } |
| function log(...args : any[]) { | |
| switch(args.length) { | |
| case 1: | |
| return logClass.apply(this, args); | |
| case 2: | |
| return logProperty.apply(this, args); | |
| case 3: | |
| if(typeof args[2] === "number") { | |
| return logParameter.apply(this, args); | |
| } | |
| return logMethod.apply(this, args); | |
| default: | |
| throw new Error(); | |
| } | |
| } |
| function logMethod(target, key, descriptor) { | |
| // save a reference to the original method this way we keep the values currently in the | |
| // descriptor and don't overwrite what another decorator might have done to the descriptor. | |
| if(descriptor === undefined) { | |
| descriptor = Object.getOwnPropertyDescriptor(target, key); | |
| } | |
| var originalMethod = descriptor.value; | |
| //editing the descriptor/value parameter | |
| descriptor.value = function () { | |
| var args = []; | |
| for (var _i = 0; _i < arguments.length; _i++) { | |
| args[_i - 0] = arguments[_i]; | |
| } | |
| var a = args.map(function (a) { return JSON.stringify(a); }).join(); | |
| // note usage of originalMethod here | |
| var result = originalMethod.apply(this, args); | |
| var r = JSON.stringify(result); | |
| console.log("Call: " + key + "(" + a + ") => " + r); | |
| return result; | |
| }; | |
| // return edited descriptor as opposed to overwriting the descriptor | |
| return descriptor; | |
| } | |
| class Person { | |
| public name: string; | |
| public surname: string; | |
| constructor(name : string, surname : string) { | |
| this.name = name; | |
| this.surname = surname; | |
| } | |
| @logMethod | |
| public saySomething(something : string, somethingElse : string) : string { | |
| return this.name + " " + this.surname + " says: " + something + " " + somethingElse; | |
| } | |
| } | |
| var p = new Person("remo", "jansen"); | |
| p.saySomething("I love playing", "halo"); |
| function logParameter(target: any, key : string, index : number) { | |
| var metadataKey = `__log_${key}_parameters`; | |
| if (Array.isArray(target[metadataKey])) { | |
| target[metadataKey].push(index); | |
| } | |
| else { | |
| target[metadataKey] = [index]; | |
| } | |
| } | |
| function logMethod(target, key, descriptor) { | |
| if(descriptor === undefined) { | |
| descriptor = Object.getOwnPropertyDescriptor(target, key); | |
| } | |
| var originalMethod = descriptor.value; | |
| //editing the descriptor/value parameter | |
| descriptor.value = function (...args: any[]) { | |
| var metadataKey = `__log_${key}_parameters`; | |
| var indices = target[metadataKey]; | |
| if (Array.isArray(indices)) { | |
| for (var i = 0; i < args.length; i++) { | |
| if (indices.indexOf(i) !== -1) { | |
| var arg = args[i]; | |
| var argStr = JSON.stringify(arg) || arg.toString(); | |
| console.log(`${key} arg[${i}]: ${argStr}`); | |
| } | |
| } | |
| var result = originalMethod.apply(this, args); | |
| return result; | |
| } | |
| else { | |
| var a = args.map(a => (JSON.stringify(a) || a.toString())).join(); | |
| var result = originalMethod.apply(this, args); | |
| var r = JSON.stringify(result); | |
| console.log(`Call: ${key}(${a}) => ${r}`); | |
| return result; | |
| } | |
| } | |
| // return edited descriptor as opposed to overwriting the descriptor | |
| return descriptor; | |
| } | |
| class Person { | |
| public name: string; | |
| public surname: string; | |
| constructor(name : string, surname : string) { | |
| this.name = name; | |
| this.surname = surname; | |
| } | |
| @logMethod | |
| public saySomething(@logParameter something : string, somethingElse : string) : string { | |
| return this.name + " " + this.surname + " says: " + something + " " + somethingElse; | |
| } | |
| } | |
| var p = new Person("remo", "jansen"); | |
| p.saySomething("I love playing", "halo"); |
| function logProperty(target: any, key: string) { | |
| // property value | |
| var _val = this[key]; | |
| // property getter | |
| var getter = function () { | |
| console.log(`Get: ${key} => ${_val}`); | |
| return _val; | |
| }; | |
| // property setter | |
| var setter = function (newVal) { | |
| console.log(`Set: ${key} => ${newVal}`); | |
| _val = newVal; | |
| }; | |
| // Delete property. | |
| if (delete this[key]) { | |
| // Create new property with getter and setter | |
| Object.defineProperty(target, key, { | |
| get: getter, | |
| set: setter, | |
| enumerable: true, | |
| configurable: true | |
| }); | |
| } | |
| } | |
| class Person { | |
| @logProperty | |
| public name: string; | |
| public surname: string; | |
| constructor(name : string, surname : string) { | |
| this.name = name; | |
| this.surname = surname; | |
| } | |
| } | |
| var p = new Person("remo", "Jansen"); | |
| p.name = "Remo"; | |
| var n = p.name; |
| function logParamTypes(target : any, key : string) { | |
| var types = Reflect.getMetadata("design:paramtypes", target, key); | |
| var s = types.map(a => a.name).join(); | |
| console.log(`${key} param types: ${s}`); | |
| } | |
| class Foo {} | |
| interface IFoo {} | |
| class Demo{ | |
| @logParameters | |
| doSomething( | |
| param1 : string, | |
| param2 : number, | |
| param3 : Foo, | |
| param4 : { test : string }, | |
| param5 : IFoo, | |
| param6 : Function, | |
| param7 : (a : number) => void, | |
| ) : number { | |
| return 1 | |
| } | |
| } | |
| // doSomething param types: String, Number, Foo, Object, Object, Function, Function |
I got the property decorator to work
export function logProperty() { return (target: any, key: string) => { // property value let _val = this[key]; // property getter function getter() { console.log(`Get: ${key} => ${_val}`); return _val; } // property setter function setter(newVal) { console.log(`Set: ${key} => ${newVal}`); _val = newVal; } // Delete property. if (delete this[key]) { // Create new property with getter and setter Object.defineProperty(target, key, { get: getter, set: setter, enumerable: true, configurable: true, }); } }; } export class MyClass { @logProperty() public name: string = 'ThatGuy'; }
This doesn't work because all instances then share the same property value - not good :). You can easily verify using the TypeScript playground.
The fix is to define a backing field as an additional property:
function logProperty(target: any, key: string) {
delete target[key];
const backingField = "_" + key;
Object.defineProperty(target, backingField, {
writable: true,
enumerable: true,
configurable: true
});
// property getter
const getter = function (this: any) {
const currVal = this[backingField];
console.log(`Get: ${key} => ${currVal}`);
return currVal;
};
// property setter
const setter = function (this: any, newVal: any) {
console.log(`Set: ${key} => ${newVal}`);
this[backingField] = newVal;
};
// Create new property with getter and setter
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
}
var p1 = new Person("remo", "Jansen");
var p2 = new Person("elon", "Musk");
console.log(p1.name);
console.log(p2.name);
p1.name = "Remo";
p2.name = "Elon";
console.log(p1.name);
console.log(p2.name);
Unfortunately the backing field still appears in for...in even if enumerable: false, so I left it true anyways. And of course the backing field can be set directly... I don't know how to prevent this.
@afr1983 Don't call Object.defineProperty for backingField. This adds the property to the prototype and you are not using it. When you do this[backingField], you are accessing an instance property.
The backingField is expected to show up in for..in, this is the behavior if you manually create a property.
Hi guys sorry but the decorators signatures are a bit different since I wrote this. You can fins the new signatures here:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void; declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;I will try to update this in the future but right now I don't have time :( Feel free to fork and send a PR if you do!
@remojansen what does the <T> at the start of the type alias for MethodDecorator mean? How is declare type MethodDecorator = <T>... different from declare type MethodDecorator<T> = ...? Is there a way to specialize it so that MethodDecorator works only for functions of a given type?
https://gist.github.com/remojansen/16c661a7afd68e22ac6e#file-method_decorator-ts-L5
Can you explain why descriptor could be undefined here?
Useful examples! Can you @remojansen please provide an example of decorator of async class method that executes only on Promise.resolve()? That will be really useful for sending analytics, for example.
Be of great help! Thanks for sharing!