Search code examples
typescriptreflect-metadata

Why is `this` undefined when passing a Function into Metadata and how to fix it?


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

Solution

  • 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");