Daddy Pumpkin
Daddy Pumpkin

Reputation: 484

TypeScript Builder Pattern similar to Java

I'm trying to implement the Builder Patten similar to Java in TypeScript 4.6.2. From what I have read, this isn't the preferred way to do it, but I have some constrains regarding the module exports.

export class HttpRequest {

    static Builder = class {

        private body: string;

        setBody(body: string): HttpRequest.Builder {
            this.body = body;
            return this;
        }

        build(): HttpRequest {
            return new HttpRequest(this);
        }

    }

    private readonly body: string;

    constructor(builder: any) {
        this.body = builder.body;
    }

    getBody(): string {
        return this.body;
    }

}

It seems that the setBody method can't return a HttpRequest.Builder type, with the following error:

TS2702: 'HttpRequest' only refers to a type, but is being used as a namespace here.

It seems to work fine if // @ts-ignore is present before the method declaration, or if the method signature is changed to setBody(body: string): any.

Is there any way to implement this with nested classes? I'm trying to avoid having two different classes, one for HttpRequest and one for Builder.

Upvotes: 3

Views: 1151

Answers (2)

Owen Pattison
Owen Pattison

Reputation: 314

In my experience using a fluent API has served me best when writing typescript builders, here's how I would do it in more of a typescript way.

interface HttpRequest {
    body: string;
}

export class HttpRequestBuilder {
    private body: string = "";

    public withBody(newBody: string): HttpRequestBuilder {
        this.body = newBody;
        return this;
    }

    public build(): HttpRequest {
        return {
            body: this.body
        }
    } 
}

Then to use this you can just new up the class

const httpRequest = new HttpRequestBuilder()
    .withBody("add a body here")
    .build();

This pattern also has several other options to be extended further.

Update: here is a hypothetical example extension to give you even more versatility.

export interface HttpRequest {
    body: string;
    contentType: string;
}

export interface HttpRequestExtension extends HttpRequest {
    message: string;
}

export class HttpRequestBuilder {
    private body: string = "";
    private contentType: string = "";

    public withBody(newBody: string): HttpRequestBuilder {
        this.body = newBody;
        return this;
    }

    public withContentType(newContentType: string): HttpRequestBuilder {
        this.contentType = newContentType;
        return this;
    }

    public build(): HttpRequest {
        return {
            body: this.body,
            contentType: this.contentType
        }
    } 
}

export class HttpRequestExtensionBuilder extends HttpRequestBuilder {
    private message: string = "";

    public withMessage(newMessage: string): HttpRequestExtensionBuilder {
        this.message = newMessage;
        return this;
    }

    public build(): HttpRequestExtension {
        return {
            ...super.build(),
            message: this.message
        }
    }
}

This way we can use either builder:

const httpRequest = new HttpRequestBuilder()
    .withBody("add a body here")
    .withContentType("text/html")
    .build();

const httpRequest = new HttpRequestExtensionBuilder()
    .withMessage("add a message here")
    .withBody("add a body here")
    .withContentType("text/html")
    .build();

Upvotes: 1

Fatih Ersoy
Fatih Ersoy

Reputation: 729

I wouldn't prefer it to use builder with static classes. You can refer here to access a playground template.

If you want to achieve it in your way do this:

class HttpRequest {

    static Builder = class {

        public body!: string;

        setBody(body: string): typeof HttpRequest.Builder {
            this.body = body;
            return this as any;
        }

        build(): HttpRequest {
            return new HttpRequest(this);
        }

    }

    private readonly body: string;

    constructor(builder: any) {
        this.body = builder.body;
    }

    getBody(): string {
        return this.body;
    }

}

But instead you can practice full pattern with:

interface IHttpRequest
{
 setRequest(body:any):void
}

class HttpRequest implements IHttpRequest
{
  body!:string;
  setRequest(body:string){
    console.log('hook: setRequest called')
    this.body = body;
  }
}

interface IHttpBuilder{
  buildRequest(body:string): void
}

class HttpBuilder implements IHttpBuilder{
  private http = new HttpRequest();
  buildRequest(body:string){
    console.log('hook: buildRequest called')
    this.http.setRequest(body);
  }
}

class Basement{
  private http!: IHttpBuilder;
  constructor(http:IHttpBuilder){
    this.http = http;
  }
  build(body:string){
    console.log('hook: build called')
    this.http.buildRequest(body);
  }
}

let httpReq = new HttpBuilder();
let builder = new Basement(httpReq);

let body = "i am body!";
builder.build(body);

Upvotes: 1

Related Questions