-
-
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 |
@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.
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:
Unfortunately the backing field still appears in
for...in
even ifenumerable: 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.