Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get class constructor argument names

I'm implementing a certain dependency injection solution for ES classes, and for it I need to know exact names of class constructor parameters.

When I get string form of class or class static method, it does give full code (as usually for functions), but for class constructor it does not.

class C { constructor(a, b) { }; static m(x,y) { } }
console.log(C);
console.log(C.constructor);
console.log(C.m);

results in

class C { constructor(a, b) { }; static m(x,y) { } }
ƒ Function() { [native code] }
ƒ m(x,y) { }

As result, I have to parse whole class code to extract constructor arguments part, using regex like this

C.toString().split(/constructor\s*[^\(]*\(\s*([^\)]*)\)/m)

Is there any cleaner way to get constructor argument names?

Update: Thank you for all your opinions and comments but I'm crystal aware how minification works, how TS decorators work and how Angular/AngularJS DI is implemented and how it works. It is not related to the question. The question is:

Is it possible to get constructor code as it is possible for function?

like image 579
setec Avatar asked Sep 12 '19 11:09

setec


2 Answers

A parameter's name should not be considered as a reliable way to identify what to inject into a constructor. As you noticed, JavaScript is not designed to let you retrieve these names in a proper way, and if you're in a browser context, you're probably minifying the code before releasing it, causing the loss of the names.

Dependency injection mechanisms in JavaScript usually rely on metadata and custom build processes, such as Angular's class metadata compiler that introspects the source code to generate runtime code.

That being said, if you're using TypeScript, here is a minimal example of how to achieve dependency injection with parameter decorators, by attaching metadata to the class itself:

const metadataKey = Symbol();

interface InjectableClass {
    new (...args: any[]): any;
    [metadataKey]: any[];
}

function Inject(value: any) {
    return function (target: InjectableClass, key: PropertyKey, paramIndex: number) {
        target[metadataKey] = Object.assign(target[metadataKey] || [], { [paramIndex]: value });
    }
}

class Test {
    static [metadataKey]: any[];
    greetings: string;
    name: string;

    constructor(@Inject('Hello there!') greetings: string, @Inject('Guerric') name: string) {
        this.greetings = greetings;
        this.name = name;
    }

    sayHello() {
        console.log(`${this.greetings} My name is ${this.name}`);
    }
}

function factory<T extends InjectableClass>(clazz: T): InstanceType<T> {
    return new clazz(...clazz[metadataKey]);
}

factory(Test).sayHello();

Which produces the following JavaScript:

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
const metadataKey = Symbol();
function Inject(value) {
    return function (target, key, paramIndex) {
        target[metadataKey] = Object.assign(target[metadataKey] || [], { [paramIndex]: value });
    };
}
let Test = class Test {
    constructor(greetings, name) {
        this.greetings = greetings;
        this.name = name;
    }
    sayHello() {
        console.log(`${this.greetings} My name is ${this.name}`);
    }
};
Test = __decorate([
    __param(0, Inject('Hello there!')),
    __param(1, Inject('Guerric')),
    __metadata("design:paramtypes", [String, String])
], Test);
function factory(clazz) {
    return new clazz(...clazz[metadataKey]);
}
factory(Test).sayHello();

TypeScript playground

Variant that uses a dedicated Map in order to store the metadata instead of attaching them to the classes:

const metadataMap = new Map();

interface Constructable {
    new (...args: any[]): any;
}

function Inject(value: any) {
    return function (target: Constructable, key: PropertyKey, paramIndex: number) {
        metadataMap.set(target, Object.assign(metadataMap.get(target) || [], { [paramIndex]: value }));
    }
}

class Test {
    greetings: string;
    name: string;

    constructor(@Inject('Hello there!') greetings: string, @Inject('Guerric') name: string) {
        this.greetings = greetings;
        this.name = name;
    }

    sayHello() {
        console.log(`${this.greetings} My name is ${this.name}`);
    }
}

function factory<T extends Constructable>(clazz: T): InstanceType<T> {
    return new clazz(...metadataMap.get(clazz));
}

factory(Test).sayHello();

TypeScript playground

like image 96
Guerric P Avatar answered Oct 08 '22 15:10

Guerric P


See here: https://replit.com/@trevorhreed/parse-class-constructor-params#index.js

var acorn = require("acorn")

class A {
  constructor(p1, p2) { }
}

const getClassConstructorParams = cls => {
  const ast = acorn.parse(cls.toString(), {
    ecmaVersion: 2020
  })
  return ast.body[0].body.body
    .find(x => {
      return x.type === 'MethodDefinition'
      && x.kind === 'constructor'
    })
    .value
    .params.map(x => x.name)
}


console.dir(
  getClassConstructorParams(A)
)
like image 26
Trevor Avatar answered Oct 08 '22 15:10

Trevor