2

I want to create an abstraction with the help of Typescript decorators and reflect-metadata. But when I invoke the function I pass into the metadata, this is undefined:

import "reflect-metadata";

const METHODS = "__methods__";
const Method = (obj: object) => {
  return function MethodDecorator(
    target: object,
    methodName: string | symbol,
    descriptor: PropertyDescriptor
  ) {
    const metadata = Reflect.getMetadata(METHODS, obj);

    Reflect.defineMetadata(
      METHODS,
      { ...metadata, [methodName]: descriptor.value },
      obj
    );
  };
};

const someObject: object = new Object();
class Main {
  private num = 42;

  constructor(other: Other) {
    other.init(someObject);
  }

  @Method(someObject)
  myMethod() {
    console.log("hello");
    console.log(this.num); // this is undefined (how can I fix it?)
  }
}

class Other {
  private methods: Record<string, Function> = {};

  init(obj: object) {
    this.methods = Reflect.getMetadata(METHODS, obj);
  }

  trigger(methodName: string) {
    this.methods[methodName]();
  }
}

const other = new Other();
new Main(other);
other.trigger("myMethod");

The output of the code snippet above is

hello
undefined

Why is this undefined and how can I fix it?


You can try it yourself by cloning this sample repo and running

yarn install
yarn start
Florian Ludewig
  • 4,338
  • 11
  • 71
  • 137
  • did you tried it with `.bind()`? `myMethod() { ... }.bind(this)` – bill.gates Jun 05 '20 at 14:12
  • @Ifaruki I thought about that. But I am not sure how this can be done in my example, because I don't have a reference to `this` inside `MethodDecorator`. Maybe with the help of `target`? – Florian Ludewig Jun 05 '20 at 14:16
  • Hmm ot you just put `this.num =4;` inside your constructor? – bill.gates Jun 05 '20 at 16:13
  • @Ifaruki It's just an example. In my real use case there are values assigned at runtime – Florian Ludewig Jun 05 '20 at 16:43
  • I don't totally understand you question but I do know this: You can call a function with a custom this like: `myfunc.call(this)` OR, a solution that I think is more likely, is that ES6 arrow functions actually modify the `this` keyword from what we're used to. Try using a normal function instead of the arrow function. – VirxEC Jun 10 '20 at 15:23
  • btw, I'm talking about the `Method` arrow function on line 4 – VirxEC Jun 10 '20 at 15:24
  • @VirxEC The arrow function is what @Terry has also proposed, but this doesn't seem to work. The `.call(this)` seems like a good idea. However for it to work the `this` has to be passed to the `Other` class which is similar to the answer by @jdaz – Florian Ludewig Jun 10 '20 at 17:45

2 Answers2

3

If you save the value of this by passing it to other.init, as below, and then bind that value to each method, it will work. Unfortunately it does not seem possible to pass this directly to the decorator, though that would be much cleaner.

const someObject: object = new Object();
class Main {
  private num = 42;

  constructor(other: Other) {
    other.init(someObject, this);
  }

  @Method(someObject)
  myMethod() {
    console.log("hello");
    console.log(this.num); // 42
  }
}

class Other {
  private methods: Record<string, Function> = {};

  init(obj: object, thisArg: object) {
    this.methods = Reflect.getMetadata(METHODS, obj);
    Object.keys(this.methods).forEach((method) => {
      this.methods[method] = this.methods[method].bind(thisArg);
    })
  }

  trigger(methodName: string) {
    this.methods[methodName]();
  }
}

const other = new Other();
new Main(other);
other.trigger("myMethod");
jdaz
  • 5,964
  • 2
  • 22
  • 34
  • 1
    This is certainly not beatiful, but it works! I'll wait a few days if someone comes up with a better approach tough :) Thank you – Florian Ludewig Jun 06 '20 at 04:16
0

That is because you're returning a function instead of an arrow function on this line:

return function MethodDecorator(

This creates a new context (function scope), which causes this to point to the global object instead. this is defined: it is this.num that is undefined, since window.num does not exist.

Thereofre, if you return the function as an arrow function, the lexical this will be preserved and you will have access to this.num:

const Method = (obj: object) => {
  return (
    target: object,
    methodName: string | symbol,
    descriptor: PropertyDescriptor
  ) => {
    const metadata = Reflect.getMetadata(METHODS, obj);

    Reflect.defineMetadata(
      METHODS,
      { ...metadata, [methodName]: descriptor.value },
      obj
    );
  };
};
Terry
  • 63,248
  • 15
  • 96
  • 118