'Is there something like Lombok for TypeScript?

I'm looking for a way to reduce the boilerplate code of my NodeJS backend. In Lombok there is e.g. the possibility to inject constructors and getter/setter by annotation for objects.

Is there a way to do this in TypeScript?



Solution 1:[1]

I googled it quickly and found projects like this which attempt to bring Lombok-like capabilities to TypeScript, but as you can see, those project are scarce and not that widely used. That implies a question: Why would you want a tool like that?

TS is already pretty good in reducing boilerplate. When I define a class, I usually do it like this:

class A {
  constructor(private fieldA: string, private readonly fieldB = 0) {}
}

This is quite concise, isn't it? I guess you are comparing capabilities of TS to those of Java. Java is very wordy and Lombok helps with that greatly. But TS and JS are different, and while some problems, which Lombok solves, are solved by TS already, others are not an issue in the world of TS and JS.

First of all, the syntax above creates class fields of certain types, with access modifiers and you can also spot the readonly keyword in front of fieldB and its default value 0. On top of that, those are created together with a constructor, which implicitly assigns values to instance fields upon execution (see, there is no this.fieldA = fieldA). So this already more than covers the Lombok's capability to inject constructors. Note on this: In JS (and therefore in TS), you can have only single constructor. JS doesn't support method overloading.

Now about the getters/setters, those are not used the same way in JS (or TS) as they are in Java. In JS, it is a norm that you are working with fields directly, and the setters and getters are used only in special cases where you want to:

  1. Forbid setting a value to an object's property in runtime by defining only a getter. Now this is usually a bit of an overkill, and since you use TS, you can just declare the field as readonly and compiler will make sure you don't assign to that property - no need to use a getter. If you develop in JS without compile time checks, the convention is to mark properties that are private (those you definitely shouldn't modify) with underscore. Either way, it can still happen that you modify a variable that you aren't supposed to modify, but unlike in Java, this is not deemed a reason good enough to use get/set everywhere in JS (and TS). Instead, if you really need to be certain that no modifications happen in runtime, you either use the aforementioned getter without setter, or you configure the object's property as non-writable.
  2. Having a custom logic in set/get functions is the other good reason to employ them. A common use case for this is a getter that is computed out of multiple variables but you still want it to look like an field on an object. That's because in JS, when you invoke a getter, you don't actually use () after the getter name. Now because this logic is custom, it can't be generated just by using an annotation.

So as you can see, some problems Lombok deals with in Java are already dealt with in TS and others are non-issues.


Edit 5-9-2021 - answer to @Reijo's question: Lomboks functionality goes beyond getters/setters/constructors. Looking at the @Builder Annotation, I am interested in what you would say about this.

If the question is just about whether there is a TypeScript/JavaScript library that offers more or less the same collection of utilities as Lombok for Java, to my knowledge the answer is NO. I think partly it is due to capabilities that TypeScript provides out of the box (as I already outlined above), which brings me back to the point that Java needs Lombok more than languages like TypeScript or Groovy do. When you need something that TS doesn't provide, like the builder pattern, you can use libraries solving a particular problem, like builder-pattern (using JS Proxy in its core) or thanks to the flexible nature of JS (and in effect TS) write it on your own easily.

That's all nice, but you'd perhaps like to add functionality in more declarative way - via annotations (in TS world those are called decorators and they work differently), as Lombok does it. This might prove complex.

First of all, if you modify the type via decorator in TS, TS compiler doesn't recognize the change. So if you augment class by, let's say, adding a method to it in your decorator, TS won't recognize that new method. See this discussion for details.

This means that you either give up on decorators and use functions instead to modify the type (which you can), or you dive into AST. That's btw how Lombok works. It takes annotated types in a compilation phase called annotation processing and thanks to a hack in javac (and Eclipse compiler) modifies their AST (to eg. create an inner builder for given class). One could do it in a somewhat similar way with TS/JS.

Though there is nothing like annotations processing in TS nor JS as such, you could still create a build step that takes a source code and modifies it's AST to achieve your goals (which is how Babel works too). This might result in adding a method to a class, generating a builder etc. based on an annotation (in a broad sense - not necessarily a decorator) used.

This approach is a challenge though. Besides AST being an advanced topic, even if you get it working, you'd need support from your IDE, which nowadays also means support from language servers. And that's not for the faint of heart.

However, my edit is not supposed to scare anyone away if you plan to create something like Lombok for TS, since it seems quite some people would like to see it in TS/JS world. It should only show you what lies ahead ;).

Solution 2:[2]

I found an alternative to @Getters and @Setters Lombok decorators.

Try this :

import { capitalize } from "lodash";

const Getters = () => <T extends {new(...args:any[]):{}}>(constructor:T) => {
  return class extends constructor {
    constructor(...args: any[]) {
      super(...args);
      const props = Reflect.ownKeys(this);
      props.forEach((prop: string) => {
        const capitalizedKey = capitalize(prop);
        const methodName = `get${capitalizedKey}`;
        Object.defineProperty(this, methodName, { value: () => this[prop] });
      });
    }
  }
}
const Setters = () => <T extends {new(...args:any[]):{}}>(constructor:T) => {
  return class extends constructor {
    constructor(...args: any[]) {
      super(...args);
      const props = Reflect.ownKeys(this);
      props.forEach((prop: string) => {
        const capitalizedKey = capitalize(prop);
        const methodName = `set${capitalizedKey}`;
        Object.defineProperty(this, methodName, { value: (newValue: any) => { this[prop] = newValue } });
      });
    }
  }
}

@Getters()
@Setters()
export class Person {
  [x: string]: any;
  nom: string;
  prenom: string;

  constructor(nom: string, prenom: string) {
    this.nom = nom;
    this.prenom = prenom;
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Ugo Evola