thedayturns
thedayturns

Reputation: 10823

How to properly wrap constructors with decorators in TypeScript

The process of wrapping a class with a decorator causes superclasses to be unable to access that classes' properties. Why?

I have some code that:

  1. Creates a decorator which replaces the constructor of a class with a new constructor that should do the exact same thing.
  2. Creates a base class with a property.
  3. Wraps the base class with the wrapping decorator.
  4. Creates a class that extends the base class.
  5. Tries to access the property on the extended class. 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'm sure this is some nuance of either prototypes in general or the specific way that TypeScript handles them that I do not grasp.

Upvotes: 27

Views: 32054

Answers (5)

TSV
TSV

Reputation: 7641

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);
    return new original(...args); // according the comments
  }

  // 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)

Upvotes: 20

etech
etech

Reputation: 2761

This is the more modern approach using the latest TS (3.2.4). The below also uses the decorator factory pattern so you can pass in 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 info: https://www.typescriptlang.org/docs/handbook/decorators.html#class-decorators

Upvotes: 26

urish
urish

Reputation: 9033

A 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

Upvotes: 13

PhiLho
PhiLho

Reputation: 41132

The comments in the other answers complain that code doesn't work.
Actually, it works, but not in jsFiddle...
It is an issue with the code generation in jsFiddle (perhaps using an obsolete version of TypeScript).
The code above works with TypeScript 2.7.2 (run with Node).

So this is basically the code in pablorsk's answer (except there is no need to return the instance), I just added full types to please a stricter 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 copying static members, otherwise decorated class throws an error when calling the static method.

Upvotes: 2

pablorsk
pablorsk

Reputation: 4276

If you like run code after and before constructor() with a 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
*/

Upvotes: 0

Related Questions