- 
            
      
        
      
    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 | 
Hi @kristianmandrup, @dulowski-marek, I am having the same problem. Does any of you found any solution?
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';
}
    Be of great help! Thanks for sharing!
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.
I quite often get the error Converting circular structure to JSON on this code - where it calls JSON.stringify() if the object passed has cyclical data structures (most DOM elements have this)
Not a simple problem to solve either - I wonder if we should be using stringify()
https://stackoverflow.com/questions/11616630/json-stringify-avoid-typeerror-converting-circular-structure-to-json