Expanding constructor in TypeScript

Can the process described in this answer be achieved using Typescript?

Subclassing a Java Builder class

This is the base class I have implemented so far:

export class ProfileBuilder {
    name: string;

    withName(value: string): ProfileBuilder {
        this.name= value;
        return this;
    }

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

export class Profile {
    private name: string;

    constructor(builder: ProfileBuilder) {
        this.name = builder.Name;
    }
}

And here is the extended class:

export class CustomerBuilder extends ProfileBuilder  {
    email: string;

    withEmail(value: string): ProfileBuilder {
        this.email = value;
        return this;
    }

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

export class Customer extends Profile {
    private email: string;

    constructor(builder: CustomerBuilder) {
        super(builder);
        this.email= builder.email;
    }
}

Similar to what was mentioned in another discussion, building a Customer instance in this order won't work due to the change of context:

let customer: Customer = new CustomerBuilder().withName('John')
                                              .withEmail('<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="86ece9eee8c6e3ebe7efeaa8e5e9eb">[email protected]</a>')
                                              .build();

I am currently exploring the use of generics to resolve this issue, but encountering difficulties when returning the 'this' pointer in my setter methods (type this is not assignable to type T). Any suggestions?

Answer №1

Successfully resolved the issue! By reviewing multiple answers in a related thread, I came up with a solution by creating a base abstract class and builder that extends for each of my class/builder pair:

abstract class BaseProfileBuilder<T extends BaseProfile, B extends BaseProfileBuilder<T, B>> {
    protected object: T;
    protected thisPointer: B;

    protected abstract createObject(): T;

    protected abstract getThisPointer(): B;

    constructor() {
        this.object = this.createObject();
        this.thisPointer = this.getThisPointer();
    }

    withName(value: string): B {
        this.object.name = value;
        return this.thisPointer;
    }

    build(): T {
        return this.object;
    }
}

abstract class BaseProfile {
    name: string;
}

class ProfileBuilder extends BaseProfileBuilder<Profile, ProfileBuilder> {
    createObject(): Profile {
        return new Profile();
    }

    getThisPointer(): ProfileBuilder {
        return this;
    }
}

class Profile extends BaseProfile {
}

class CustomerBuilder extends BaseProfileBuilder<Customer, CustomerBuilder>  {
    createObject(): Customer {
        return new Customer();
    }

    getThisPointer(): CustomerBuilder {
        return this;
    }

    withEmail(value: string): CustomerBuilder {
        this.object.email = value;
        return this;
    }
}

class Customer extends BaseProfile {
    email: string;
}


let customer: Customer = new CustomerBuilder().withName('John')
                                              .withEmail('<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="62080d0a0c22070f030b0e4c010d0f">[email protected]</a>')
                                              .build();

console.log(customer);

Answer №2

As I faced a similar requirement recently, I devised a solution that involves creating a profile builder class which can be extended from the customer builder. By using super, we can call the base builder within this setup.

class ProfileBuilder {
    private name: string;

    constructor() {
        this.name = undefined;
    }

    public withName(name: string) {
        this.name = name;
        return this;
    }

    public build() {
        return {
            name: this.name
        }
    }
}

class CustomerBuilder extends ProfileBuilder {
    private email: string;

    constructor() {
        super();

        this.email = undefined;
    }

    public withEmail(email: string) {
        this.email = email;
        return this;
    }

    public build() {
        const base = super.build();
        return {
            ...base,
            email: this.email
        }
    }
}

With this structure, you can now create a customer based on your specifications:

const customer = new CustomerBuilder()
    .withName("John")
    .withEmail("<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="aac0c5c2c4eacfc7cbc3c684c9c5c7">[email protected]</a>") 
    .build();

Answer №3

Ensure the return type of chaining methods is set to this

Within classes, the special type known as this dynamically refers to the type of the current class.

-- Typescript Docs

By setting the return type of chained methods to match the type of the builder instance you created, it grants complete access to all available methods on that specific builder. This includes inherited, overridden, or added methods in a subclass.

// -----------
// Parent Class

class ProfileBuilder {
  name?: string;
  
  // The `this` return type will dynamically match the instance type
  withName(value: string): this {
    this.name = value;
    return this;
  }

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

class Profile {
  private name: string;

  constructor(builder: ProfileBuilder) {
    this.name = builder.name ?? 'default name';
  }
}


// -----------
// Child class

class CustomerBuilder extends ProfileBuilder {
  email?: string;

  // Return `this` here too to allow further subclassing.
  withEmail(value: string): this {
    this.email = value;
    return this;
  }

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

class Customer extends Profile {
  private email: string;

  constructor(builder: CustomerBuilder) {
    super(builder);
    this.email = builder.email ?? '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9efafbf8ffebf2eadefbf3fff7f2b0fdf1f3">[email protected]</a>';
  }
}



// -----------
// Example Usage

// Notice that the order of the `with` methods no longer matters.

let customer: Customer = new CustomerBuilder()
  .withName('John')
  .withEmail('<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e3898c8b8da3868e828a8a8fcd808c8e">[email protected]</a>')
  .build();

let customer2: Customer = new CustomerBuilder()
  .withEmail('<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1379727d7653767e727a7f3d707c7e">[email protected]</a>')
  .withName('Jane')
  .build();

export {};

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular Material calendar tool customization for designated input

Is it possible to individually control the format of input for a datepicker without affecting the format for the entire module? <input matInput [matDatepicker]="dp" [formControl]="date" [format]="'DD/MM/YYYY'"> <-- Can this be done? < ...

Unable to get md-virtual-repeat to work within md-select?

Attempting to use md-select to showcase a large amount of data is causing the browser to freeze upon opening. To address this, I tried implementing md-virtual repeat within md-select for improved performance. However, the code doesn't seem to be funct ...

Typescript - using optional type predicates

I'm looking to create a custom type predicate function that can accurately determine if a number is real and tighten the type as well: function isRealNumber(input: number | undefined | null): input is number { return input !== undefined && ...

Angular Material (8) error code S2591: The variable 'require' is not defined in the current scope

Currently, I am attempting to record the date and time in the JavaScript console. Despite the code successfully logging the dates, an error message persists: Note: The code is functioning properly, with the dates being displayed in the console. It is only ...

Obtaining user profile pictures from Graph API for several users at the same time with the help of RxJs

I have created an ASP.NET Core API that can provide a list of users to an Angular service. The user data returned consists of certain properties like id, firstName, and lastName. My aim is for the Angular service to first fetch this user list from my API ...

Using TypeScript in .cshtml Razor Files

Recently, I embarked on a new project utilizing the ASP.NET-MVC framework. For this particular project, I decided to opt for TypeScript over JavaScript. While Visual Studio provides excellent support for TypeScript, I encountered some compatibility issues ...

Here is an example showcasing how to use Angular 2 to make an

How can I correctly retrieve json data from an http get request in Angular 2? Currently, I am working on testing some local data with a mocked endpoint. Although I am able to see the result in the http.get() method, I am facing issues when trying to assign ...

Struggling to properly test the functionality of my NgForm call in Angular2+

I've been trying to test the login functionality by inputting username and password in an NgForm, but I keep encountering unsuccessful attempts. Is there a vital step that I may be overlooking? Currently, I'm facing this error message: Chrome 6 ...

The function has been called but it did not return a

It seems that there is confusion surrounding the .toHaveBeenCalled() Matcher in Jasmine. While it should return a Promise that resolves when the function has been called, some users are experiencing it returning undefined instead. For example: it('sh ...

Tips for sending a query using the http GET method in Next.JS 14 API routes

When using the Next.js 14 API route, I am passing a page in the GET method to paginate the data fetched from my database. However, an error is thrown when trying to retrieve the query from the request: Property 'query' does not exist on type &a ...

Troubleshooting issue with alignment in Material UI using Typescript

<Grid item xs={12} align="center"> is causing an issue for me No overload matches this call. Overload 1 of 2, '(props: { component: ElementType<any>; } & Partial<Record<Breakpoint, boolean | GridSize>> & { ...

Resolving parent routes in Angular 2

I am encountering an issue with my code. The 'new' route is a child route for the 'users' route. The 'users' route has a resolver, and everything works fine up to this point. However, after successfully creating a new user, ...

Serving Django and Angular with Apache

My setup involves using Angular for the frontend and Django Rest API for the backend. The structure of my project is as follows: example.com |- client (contains Angular files) |- server (contains Django Rest Framework files) The Angular app communica ...

Unable to access 'export default class extends Vue' in the template

I've recently started using Vue.js with TypeScript, but I'm having trouble accessing values from outside the class. @Component({ name: 'SidebarItem', components: { SidebarItemLink } }) export default class extends ...

The statement 'typeof import("...")' fails to meet the requirement of 'IEntry' constraint

When trying to run npm run build for my NextJS 13 app, I encountered the following type error: Type error: Type 'typeof import("E:/myapp/app/login/page")' does not satisfy the constraint 'IEntry'. Types of property 'def ...

What is the best way to transform HeadersInit into an Object<string,string> data type?

In short, I am faced with the task of converting the headers of a RequestInit into a format that another library can comprehend. This particular library requires the headers to be in the format of Object<string, string>. Initially, I attempted someth ...

Comparing Angular's 'ng serve' and 'npm start' commands

Just a quick query regarding angular-cli. I'm curious, is it correct that when I use ng serve, I am utilizing the global installation of angular-cli but when I opt for npm start, I am using the local one? ...

How to set up npm to utilize the maven directory format and deploy war files

Embarking on my very first pure front-end project, I decided to deploy it using Java/Maven. To begin, I set up a standard WAR project: │ package.json │ pom.xml │ tsconfig.json │ typings.json │ │ ├───src │ └───main ...

Expanding the properties of an object dynamically and 'directly' by utilizing `this` in JavaScript/TypeScript

Is it possible to directly add properties from an object "directly" to this of a class in JavaScript/TypeScript, bypassing the need to loop through the object properties and create them manually? I have attempted something like this but it doesn't se ...

Ways to resolve issues related to null type checking in TypeScript

I am encountering an issue with a property that can be null in my code. Even though I check for the value not being null and being an array before adding a new value to it, the type checker still considers the value as potentially null. Can anyone shed lig ...