javascriptnode.jstypescriptapilambda

How make method decorators instance the object in Typescript


I have been developing a lambda project and we are using lambda-api package. Then I have defined some decorators called Get and Post to map a route into lambda api object. With these decorators I have defined a class called ProductApi to hold methods that can be configured using those decorators and passing a route path. It works fine.

The problem is that when I have a class like ProductApi the constructor is never called and if I want to add some dependencies (like a Service or a Repository) it will never be defined. In this example, the /health route works fine because it does not use anything from the object instance, but other routes does not.

How can I make sure that the constructor will be called and define the service instance?

const api = createAPI();

function Get(path: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        api.get(path, descriptor.value.bind(target));
    };
}

function Post(path: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        api.post(path, descriptor.value.bind(target));
    };
}

class ProductApi {
    private someValue: string;

    constructor(private readonly productService: IProductService = new ProductService()) {
        // this scope does not execute
        this.someValue = "some value";
    }

    @Get('/health')
    async healthCheckr(req: Request, res: Response) {
        console.log(`Executing -- GET /health`);
        // this.someValue does not exists here
        return res.status(200).json({ ok: true });
    }

    @Get('/products')
    async getProducts(req: Request, res: Response) {
        console.log(`Executing -- GET /products`);
        const data = this.productService.getProductsFromService(); // error: Cannot read properties of undefined (reading 'getProductsFromService')
        return res.status(200).json(data);
    }

    @Post('/products')
    async postProducts(req: Request, res: Response) {
        console.log(`Executing -- POST /products`);
        const product = this.productService.saveProduct('Drums', 1200); // erro: Cannot read properties of undefined (reading 'saveProduct')
        return res.status(201).json(product);
    }
}

export const lambdaHandler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
    console.log('SCOPE lambda');
    return await api.run(event, context);
};

Note: I don't want to use frameworks, I just want a easy way to configure routes on the lamda api instanmce.


Solution

  • Unlike in C#, in JS a "method" is just a function stuck to an object. You can easily put it in a variable or stick it to another object. This basically defines what this is inside that "method". And a "class" constructor is just a function that creates a new object and tells it, "if someone's looking for some property that you don't have, forward them to my prototype object over here." Then it executes the code inside the constructor with that object as this.

    That is JS' prototypal inheritance in a nutshell, and even if JS has received a class keyword in the meantime, that's what still happens behind the scenes.

    Why am I explaining this?

    Because decorator are working on that prototype object. This line here api.get(path, descriptor.value.bind(target)); takes the method from that prototype, permanently binds the prototype object as this (so the resulting function will only know the prototype object and never ever see any real instance) and uses the bound function as a callback for that route.

    So currently, even if that class would magically be instantiated (by whom; I don't know) the function that you've passed to the route will have no knowledge of that.

    imo. Your decorator should look more like this:

    function Get(path: string) {
        return function (target: any, methodName: string) {
            if(typeof target[methodName] !== "function"){
              throw new Error("you need to use this decorator with a method.");
            }
    
            const Class = target.constructor;
    
            api.get(path, (req: Request, res: Response) => {
              const instance = diContainer.getInstance(Class); // or new Class();
              return instance[methodName](req, res);
            });
        };
    }
    

    Sidenote: Dimava brought up this topic; these are legacy decorators. TS adapted them long before there was a spec for decorator in JS. Now there is one and it significantly differs from these legacy decorators and TS has finally implemented the spec in V5. You (and me) should get updated on the new syntax and adopt it, because this syntax will probably be deprecated pretty soon.