-
-
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 |
In class decorator, indeed instanceof
works but a simple console.log
of the resulting object displays:
c
name: "remo"
surname: "jansen"
__proto__: Object
How to get back the name of the original class Person
instead of c
?
@dulowski-marek I'm having the same problem. Anyone knows of a resolution to this while still targeting ES5? Thanks.
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
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.
Correct me if I'm wrong but it looks like
@logProperty
decorator defines property on prototype making things a bit wrong: as I can see running code from typescript playgroundthis
is awindow
object and then once you make several objects of typePerson
, changing the name for one of them will resolve in changing the name for all the others (since this property lives in prototype)?