How to properly wrap constructors using decorators in TypeScript

The process of wrapping a class using a decorator makes superclasses not have access to the properties of these classes. Why?

I have a code that:

  • Creates a decorator that replaces the class constructor with a new constructor that should do the same.
  • Creates a base class with a property.
  • Wraps around the base class with a wrapper decorator.
  • Creates a class that extends the base class.
  • Attempts to access an extended class property. This is the part that fails.

Here is the code:

function wrap(target: any) { // the new constructor var f: any = function (...args) { return new target(); } f.prototype = target.prototype; return f; } @wrap class Base { prop: number = 5; } class Extended extends Base { constructor() { super() } } var a = new Extended() console.log(new Extended().prop) // I'm expecting 5 here, but I get undefined. 

I am sure that this is some kind of nuance of either prototypes in general, or a specific way that TypeScript processes them, which I do not understand.

+9
source share
5 answers

This code works for me:

 function logClass(target: any) { // save a reference to the original constructor var original = target; // the new constructor behaviour var f : any = function (...args) { console.log("New: " + original.name); return original.apply(this, args) } // copy prototype so intanceof operator still works f.prototype = original.prototype; // return new constructor (will override original) return f; } @logClass class Base { prop: number = 5; } class Extended extends Base { constructor() { super() } } var b = new Base() console.log(b.prop) var a = new Extended() console.log(a.prop) 
+12
source

If you like to run the code after and before the constructor () using the decorator :

 function ClassWrapper() { return function(target: any) { // save a reference to the original constructor var original = target; // the new constructor behaviour var f: any = function (...args) { console.log('ClassWrapper: before class constructor', original.name); let instance = original.apply(this, args) console.log('ClassWrapper: after class constructor', original.name); return instance; } // copy prototype so intanceof operator still works f.prototype = original.prototype; // return new constructor (will override original) return f; }; } @ClassWrapper() export class ClassExample { public constructor() { console.info('Running ClassExample constructor...'); } } let example = new ClassExample(); /* CONSOLE OUTPUT: ClassWrapper: before class constructor ClassExample Running ClassExample constructor... ClassWrapper: after class constructor ClassExample */ 
+2
source

Comments in other answers complain that the code is not working.
Actually, this works, but not in jsFiddle ...
This is a problem with code generation in jsFiddle (possibly using an older version of TypeScript).
The code above works with TypeScript 2.7.2 (runs on the node).

So this is basically the code in pablorsk's answer (except there is no need to return an instance), I just added full types to please the more stringent TSLint ...

 function logClass<T extends { new(...args: any[]): {} }>(): any { type Ctor = new (...args: any[]) => T; return (target: T): Ctor => { // Save a reference to the original constructor const Original = target; // the new constructor behaviour let decoratedConstructor: any = function (...args: any[]): void { console.log("Before construction:", Original); Original.apply(this, args); console.log("After construction"); }; // Copy prototype so intanceof operator still works decoratedConstructor.prototype = Original.prototype; // Copy static members too Object.keys(Original).forEach((name: string) => { decoratedConstructor[name] = (<any>Original)[name]; }); // Return new constructor (will override original) return decoratedConstructor; }; } @logClass() class Base { prop = 5; constructor(value: number) { console.log("Base constructor", value); this.prop *= value; } foo() { console.log("Foo", this.prop); } static s() { console.log("Static s"); } } class Extended extends Base { constructor(init: number) { super(init); console.log("Extended constructor", init); } bar() { console.log("Bar", this.prop); } } const b = new Base(2); console.log("Base", b instanceof Base); b.foo(); Base.s(); const e = new Extended(5); console.log("Extended", e instanceof Base, e instanceof Extended); e.bar(); 

[EDIT] Also added a line that copies static elements, otherwise the decorated class throws an error when calling the static method.

+2
source

Solution using ES2015 Proxy to override the constructor:

 function wrap(target: any) { return new Proxy(target, { construct(clz, args) { console.log('Constructing ${target.name}'); return Reflect.construct(clz, args); } }); } @wrap class Base { prop: number = 5; } class Extended extends Base { constructor() { super() } } var a = new Extended() console.log(new Extended().prop); 

You can also run this on StackBlitz.

+1
source

This is a more modern approach using the latest TS version (3.2.4). The decorator factory template is also used below so you can pass attributes:

 function DecoratorName(attr: any) { return function _DecoratorName<T extends {new(...args: any[]): {}}>(constr: T){ return class extends constr { constructor(...args: any[]) { super(...args) console.log('Did something after the original constructor!') console.log('Here is my attribute!', attr.attrName) } } } } 

See here for more information: https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators

0
source

Source: https://habr.com/ru/post/1238923/


All Articles