2
votes

I am currently looking at learning Typescript Decorators. My first goal is to somewhat reproduce what @Slf4J from the Project Lombok does in Java to Typescript. The ideas is to annotate/decorate a class with e.g. @logger to receive a field log of type LogUtil within that same class in order to call e.g. log.info().

LogUtil class:

export class LoggerUtil {
    logLevel: LogLevel;

    constructor(logLevel: LogLevel) {
        this.logLevel = logLevel;
    }

    error(className: string, message: string) {
        if (this.logLevel >= LogLevel.ERROR) {
            console.error(`${new Date()} [ERROR] ${className}: ${message}`);
        }
    }

    warn(className: string, message: string) {
        if (this.logLevel >= LogLevel.WARN) {
            console.log(`${new Date()} [WARN] ${className}: ${message}`);
        }
    }

    log(className: string, message: string): void {
        console.log(`${new Date()} [LOG] ${className} ${message}`)
    }

    info(className: string, message: string): void {
        if (this.logLevel >= LogLevel.INFO) {
            console.log(`${new Date()} [INFO] ${className}: ${message}`)
        }
    }

    call(className: string, message: string) {
        if (this.logLevel >= LogLevel.INFO) {
            console.log(`${new Date()} [CALL] ${className}.${message}`)
        }
    }

    debug(className: string, message: string) {
        if (this.logLevel >= LogLevel.DEBUG) {
            console.log(`${new Date()} [DEBUG] ${className}: ${message}`)
        }
    }
}

LogLevel enum:

export enum LogLevel {
    ERROR = 0,
    WARN = 1,
    INFO = 2,
    DEBUG = 3
}

Example class using the @logger decorator to get an instance of LoggerUtil as log

@logger
export class SomeService {

    exampleFunction() {
        log.info("exampleFunction called")
    }
}

I am currently trying to do this with the class-level decorators. Here I am trying to do different things:

Using the Reflect API to define a property on the class. Here I am not even sure if that even works.

export function logger() {
    return function(target: Function) {
        Reflect.defineProperty(target, "log", { value: new LoggerUtil(LogLevel.DEBUG) } )
    }
}

Using the class prototype to define a property:

export function logger() {
    return function(target: Function) {
        target.prototype.log = new LoggerUtil(LogLevel.DEBUG);
    }
}

With every approach I am getting "Cannot find name 'log'" when referencing the log instance within the Service:

@logger
export class SomeService {

    exampleFunction() {
        log.info("exampleFunction called") // Cannot find name 'log'
    }
}

Is my idea possible at all? Is there something fundamental that I am missing?

Thank you vey much in advance for any feedback!

1
Welcome to so. Maybe take a look at stackoverflow.com/questions/48599889/… - Markus
Thank you! I feel that solution does not work in my case as I want to use the method/field within the same class without creating an instance first. Thus, I cannot make use of inheritance as far as I can tell. - gerstams

1 Answers

0
votes

I found an approach that satisfies my needs at least a bit:

I went with inheritance (which I'd like to avoid). See below example:

@logger
export class SomeService extends Logger {

    constructor() {
        super()
    }

    exampleFunction() {
        log.info("exampleFunction called")
    }
}

Then the super class I am extending looks like this:

@logger
export class Logger {
    log: LoggerUtil;
}

And finally, the decorator looks like this:

export function logger<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        log = new LoggerUtil(LogLevel.DEBUG);
    }
}

My idea was to add the field log to SomeService via inheritance. To initialize this field now, I used the decorator on the super class. The decorator itself returns a class that extends the decorator class with the initialized field. This way the following inheritance graph is created:

@logger (Decorator) -> Logger (super class) -> SomeService.

I could have initialized the log field within the super class Logger itself. However, this was to look into decorators and hopefully erase the super class in the long run.

As a reference I want to point to the Typescript documentation about how to overwrite the constructor.