10

For anyone viewing this, this question is similar to the following:

How do I get the name of an object's type in JavaScript?
Get an object's class name at runtime in TypeScript

However it is different in a few regards.

I'm looking to get the name of method that belongs to a class and store it in a variable in TypeScript / JavaScript.
Take a look at the following setup:

class Foo {

    bar(){
        // logic
    }

}

The above is valid TypeScript and I would like to create a method in a different class that will return me the name of the bar() method, i.e "bar"
eg:

class ClassHelper {

    getMethodName(method: any){
        return method.name; // per say
    }

}

I would then like to be able to use the ClassHelper in the following way:

var foo = new Foo();
var barName = ClassHelper.getMethodName(foo.bar); // "bar"

I've looked at a lot of posts, some suggest using the following:

var funcNameRegex = /function (.{1,})\(/;   
var results = (funcNameRegex).exec(obj.toString());
var result = results && results.length > 1 && results[1];

but this fails as my methods do not begin with function
another suggestion was:

public getClassName() {
    var funcNameRegex = /function (.{1,})\(/;
    var results  = (funcNameRegex).exec(this["constructor"].toString());
    return (results && results.length > 1) ? results[1] : "";
}

This only returns the class name however and from reading posts, it seems using constructor can be unreliable.

Also, when I've debugged the code using some of these methods, passing in the method like so: ClassHelper.getMethodName(foo.bar); will result in the parameter being passed if the method takes one, eg:

class Foo {

    bar(param: any){
        // logic
    }

}

var foo = new Foo();
var barName = ClassHelper.getMethodName(foo.bar); // results in param getting passed through

I've been struggling with this for a while, if anyone has any information on how I can solve this it would be greatly appreciated.

My .toString() on the method passed in returns this:

.toString() = "function (param) { // code }"

rather than:

.toString() = "function bar(param) { // code }"

and according to MDN it isn't supposed to either:

That is, toString decompiles the function, and the string returned includes the function keyword, the argument list, curly braces, and the source of the function body.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString#Description

Community
  • 1
  • 1
Colum
  • 996
  • 2
  • 8
  • 23

6 Answers6

4

I have taken John White's idea and improved it so it works for every case I could think of. This method has the advantage of not needing to parse js code at runtime. There is an edge case though, where it simply can't deduce the right property name because there are multiple right property names.

class Foo {
  bar() {}
  foo() {}
}

class ClassHelper {
  static getMethodName(obj, method) {
    var methodName = null;
    Object.getOwnPropertyNames(obj).forEach(prop => {
      if (obj[prop] === method) {
        methodName = prop;
      }
    });

    if (methodName !== null) {
      return methodName;
    }
    
    var proto = Object.getPrototypeOf(obj);
    if (proto) {
      return ClassHelper.getMethodName(proto, method);
    }
    return null;
  }
}

var foo = new Foo();
console.log(ClassHelper.getMethodName(foo, foo.bar));
console.log(ClassHelper.getMethodName(Foo.prototype, foo.bar));
console.log(ClassHelper.getMethodName(Foo.prototype, Foo.prototype.bar));

var edgeCase = { bar(){}, foo(){} };
edgeCase.foo = edgeCase.bar;
console.log(ClassHelper.getMethodName(edgeCase, edgeCase.bar));
Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
3

I found a solution. I'm not sure how efficient and reusable it is, but it worked in multiple test cases, included nested methods, eg Class -> Class -> Method

My solution:

class ClassHelpers {
    getName(obj: any): string {    
        if (obj.name) {
            return obj.name;
        }

        var funcNameRegex = /function (.{1,})\(/;   
        var results = (funcNameRegex).exec(obj.toString());
        var result = results && results.length > 1 && results[1];

        if(!result){
            funcNameRegex = /return .([^;]+)/;
            results = (funcNameRegex).exec(obj.toString());
            result = results && results.length > 1 && results[1].split(".").pop();
        }

        return result || "";
    }
}


class Foo {

    bar(param: any){
        // logic
    }

}

var foo = new Foo();
var barName = ClassHelper.getMethodName(() => foo.bar);

The lambda notation ClassHelper.getMethodName(() => foo.bar); was key to getting this to work as it allowed the .toString() to contain return foo.bar;

The next thing I had to do was to extract the method call from the .toString() then I used array and string functions to return the last substring which inevitably is the method name.

Like I said, it's probably not the most elegant solution but it has worked and even worked for nested methods

NOTE: You can replace the lambda function with a regular anonymous function

var foo = new Foo();
var barName = ClassHelper.getMethodName(function() { return foo.bar; });
Colum
  • 996
  • 2
  • 8
  • 23
  • This solution may have a downside when old browsers and reserved keywords are involved, as the lambda might compile to `foo["default"]`. Otherwise looks good. – Tamas Hegedus Jun 30 '16 at 11:35
  • you could change the lambda function to a regular function i believe: `function () { return foo.bar; }` – Colum Jul 01 '16 at 08:04
1

Unfortunately, the name of Typescript class methods is lost when compiling to JS (as you correctly inferred). Typescript methods are compiled to Javascript by adding the method to the prototype of a Javascript class. (Check the compiled Javascript for more info).

In Javascript, this works:

Foo.prototype["bar"] // returns foo.bar <<the function>>

So, the thing you could think of is reversing the prototype of the class, so the class itself becomes the key of the object:

Foo.prototype[foo.bar] // return "bar"

Of course, this is a very hacky solution, since

  1. Complexity is O(N) (loop through array)
  2. I'm not sure this works in every case.

(Working) Example for your problem:

class Foo{
    bar(){}
}

class ClassHelper{
    static reversePrototype(cls:any){
        let r = {};
        for (var key in cls.prototype){
            r[cls.prototype[key]] = key;
        }
        return r;
    }

    static getMethodNameOf(cls: any, method:any):string{
        let reverseObject = ClassHelper.reversePrototype(cls);
        return reverseObject[method];
    }
}

var foo = new Foo();
console.log(ClassHelper.getMethodNameOf(Foo, foo.bar)) // "bar"

The better solution would be a typescript compiler option that changes the way typescript transpiles classes to javascript. However, I'm currently not aware of any option that does this sort of thing.

Georg Grab
  • 2,271
  • 1
  • 18
  • 28
  • this is a good work around, however it doesn't work if the class is unknown. That may seem backward and wrong, but the way this is implemented in my code, a separate class takes a parameter of the method, which then looks to try find the name. the parameter can be a method of any class. I think i may have found a solution and i am currently working on it – Colum Jun 30 '16 at 09:49
  • Don't forget that the function is converted to string representation when you put it in an object as a key. That makes different functions with the same content collide. – Tamas Hegedus Jun 30 '16 at 12:01
1

Object.keys may help you. Try this way

class Greeter {
    test : Function;

    constructor(message: string) {

        this.test = function(){
            return message;
        }
    }    
}

var a = new Greeter("world");

var properties = Object.keys(a);
alert(properties.join(', ')); //test
bCliks
  • 2,918
  • 7
  • 29
  • 52
1

You could pass in both the object and the method, get the array of property keys on the object, then using property keys see if each given property is the same object reference as the supplied method -- and if so, there is a match.

class ClassHelper {
    getMethodName(obj: any, method: any) {
        var methodName: string;

        if (method) {
            Object.keys(obj).forEach(key => {
                if (obj[key] === method) {
                    methodName = key;
                }
            });
        }

        return methodName;
    }
}

This has the downside that the host object must also be known. If the method cannot be found, undefined will be returned.

John Weisz
  • 30,137
  • 13
  • 89
  • 132
  • After trying this approach my unit tests failed, and therefore did not provide a suitable answer for my specific situation, however it's a good approach and may work for others – Colum Jun 30 '16 at 11:04
  • @Colum Could it be perhaps that the same method exists in multiple properties, or that inheritance is involved? For the latter, you could traverse the prototype chain until you reach `Object`. – John Weisz Jun 30 '16 at 12:50
0

Hi I have been making some testing and I founded a simpler solution.

class Parent{
  constructor(){
    // here you initialize your stuff
  }
   parentMethod1 (){ return "I am method 1" }
   parentMethod2 (){ return "I am method 2" }

}


class Child extends Parent{
  constructor(){
    // make this if you want to pass extra args to parent class
    super()
  }
  childMethod1(){  /*child actions*/ }
  childMethod2(){ /* other child actions */ }
}

const parent = new Parent();
const child = new Child();

console.log( Object.getOwnPropertyNames(parent.__proto__)) // --> ["constructor", "parentMethod1", "parentMethod2"]
console.log(Object.getOwnPropertyNames(child.__proto__)) //--> ["constructor", "childMethod1","childMethod2"]

console.log(parent.constructor.name) // --> Parent
console.log(child.constructor.name) // --> Child
rubendmatos1985
  • 466
  • 3
  • 13