Understanding ‘this’ in Builder classes

Builder classes are an interesting pattern. Originating from the OOP world, they seem to have found a good place in the Javascript (and Typescript) ecosystem.

Builders can be incredibly helpful in assisting us in creating complex objects, often step-by-step. They are at their best in code-bases where we often need similar, yet different, complex objects that share attributes.

Often the builder pattern utilizes an approach called Partial Object Population or Partially Initialized Entities. The two terms can be used interchangably as they mean the same, however I do believe it is fine to remember one and stick to it. But it is incredibly helpful to have some keywords ready for Google, when learning new things, such as the builder pattern 😄

Before we move on, let’s think of an example we could use. Here, we’ll be using a simple example from the book “Programming Typescript: Making your Javascript Applications Scale“:

class RequestBuilder{
  private url?: string = null; 
  private method?: "get" | "post" = null;  
  private data?: object = null;  

  setUrl(url: string){    
    this.url = url;    
    return this;  
  }  

  setMethod(method: "get" | "post"): this{ 
    this.method = method;  
    return this; 
  }  

  setData(data: object): this{ 
    this.data = data;  
    return this; 
  }  

  send(){  
    console.log(this.data," ",this.method," ",this.url); 
  }
}

Admittedly this example is a bit muddied, because with a clean builder pattern you would have one class (RequestBuilder for example) to build the request and one class (Request for example) to send the request.

However, as its presented in the book, we’ll use this one for today.

A quick rundown of the above example dictates that it expects us to run the set-methods, more or less in the same order they are defined. One might expect an example use of the above builder to look like this:

const request = new RequestBuilder().setUrl('/example').setMethod('get').setData({ hello: "world" }).send();

Seems simple enough, right? Let’s dive in.

Functional versus Object Oriented Builders

Object Oriented Programming (OOP) entails encapsulation, abstraction, inheritance, and much more. Functional Programming (FP) entails composing instead of inheritance. Functions as first-class citizens. Create pure and highly re-usable functions.

Partial Object Population introduce an interesting pattern where we often chain a lot of .setXXX(YY) methods when instantiating the object, until finally calling a “terminal method” at the end of the chain, which builds the object.

That seems very much like functional programming, right? I mean, we’re just calling function after function.

Well, to some extent! Let’s see if we can understand why.

Both paradigmes (FP & OOP) have their strengths and weaknesses. Where FP really shines is around states (of objects) and in particular, making invalid states unrepresentable.

So what do I mean by that?

Let’s imagine that in the previous example I had omitted the setUrl() method, before calling send(). That would not give me a compile time error, nor would it complain that url is missing, because the class is built in a way where it assumes that I do things in a certain way, but doesn’t enforce it.

It would simply error out! Baaaad.

Whereas with functional programming we tend to have much more explicit requirements for parameters, and with a builder-like pattern we could create a set of methods that checked for certain attributes on the object or argument that is passed in, and properly throw errors at compile time.

Or, instead of this convoluted example we could have a single method that takes in a single object as parameter and we do all the evaluations in the same function.

Understanding ‘this’

this is a bit of a particular keyword, and one that has some history. Especially in the Javascript ecosystem!

If you’ve ever worked with jQuery, you may know why the this keyword can be extremely problematic; it’s not always clear what it references.

Yikes! 👀

However, using a classes the this keyword behaves a bit more naturally. As we know from OOP that each object is an instance of a class, the this keyword becomes a reference to that instance, when called from within the class.

Going with our example code, that means that in the following block:

  setMethod(method: "get" | "post"): this{ 
    this.method = method;  
    return this; 
  }  

The this.method = method; is actually setting the method to this instance. In human language we could write it as:

Set the ‘method’ attribute for this instance (of the class) equal to the value of ‘method’ (the input parameter!).

Introducing type-safety using Typescript

Of course our ambition with Typescript is always to ensure that our compiler throws an error in case we are doing something that wouldn’t work at runtime.

There are cases of course when we are smarter than typescript and that isn’t strictly necessary.

However, let’s assume we wanted to make our code sample type safe in a way that it would throw an error at compile time, if we tried to call the send() method, before all relevant attributes are set on the class.

Let’s rewrite the class as such:

type HttpRequest = {
  url: string,
  method: "get" | "post",
  data?: object
};

class RequestBuilder{
  url?: string = null;
  method?: "get" | "post" = null;
  data?: object = null;

  setUrl(url: string){
    this.url = url;
    return this as this & {url: string};
  }

  setMethod(method: "get" | "post"){
    this.method = method;
    return this as this & {method: "get" | "post"};
  }

  setData(data: object){
    this.data = data;
    return this as this & {data: object};
  }

  send(this: HttpRequest){
    console.log(this.data," ",this.method," ",this.url);
  }
}

Firstly, you’ll notice that I have added an extra type HttpRequest which we can use to ‘hack’ our type annotations for this when calling the send method.

Essentially we are calling send with a parameter named this – which isn’t the class keyword this! – and specifying the parameter type to our new type HttpRequest.

This makes it very convenient to call the method, because it allows us to use the same wording and instead of this being dynamically typed, we are making it statically typed for this one method call, which is what allows Typescript to check for errors at compile time.

You may also notice we are now returning a little bit differently now, eg:

return this as this & {url: string};

In Typescript the & operator is used to create what’s known as Intersection Types, which is a way to combine objects types.

So in essence that means instead of returning this we are in fact returning this but defining this as an intersection type that consists of both this and {url: string}.

That’s more important than it looks like, because in the end it is actually what allows the Typescript Language Server to correctly throw errors when we attempt to call send() without first setting all the necessary values on this.

Some examples to round off with

In order to fully understand the change that was just made, let’s run a few examples

new RequestBuilder().setUrl("/example").setMethod("get").setData({ foo:"bar" }).send();

This call returns OK, as the url and method are set. Data is also set, but is optional.

new RequestBuilder().setMethod("get").setUrl("/example").send(); 

This call also returns OK, as the url and method are set. Data is not set, but as it is optional, the call will work anyway.

new RequestBuilder().setMethod("get").send();

This call returns ERROR, as the url is now missing!

new RequestBuilder().send();

This call also returns ERROR as both the url and method are missing .

new RequestBuilder().setUrl("/example").setData({ foo:"bar" }).send();

This call returns ERROR as the method is still missing, despite data being set.

new RequestBuilder().setData({ foo:"bar" }).setUrl("/example").setMethod("post").send();

In this final example, the call returns OK as both url and method is set.

Conclusion

I hope that gave some insights into how the builder pattern may be utilized in Typescript, and some insights how the tricky keyword this can be used with Classes.

Remember this was a thought out example from a book, but in the real world I would always prefer a functional approach to this sort of issue.

OOP has its place, even if many believe it doesn’t belong in the Javascript/Typescript ecosystem, it can be useful.

It’s all about making sure you have the right usecase for it, and if there are no explicit and good arguments for using OOP for a given piece of code, always prefer functional by default.

Sure, it is my preferred style in the TS/JS ecosystem so I may be slightly biased, but at the same time it is also what JS is best at. I always believe in working with the tools we are given not against them.