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

Alternatives to using wildcards in TypeScript

In my code, I have defined an interface called ColumnDef which consists of two methods: getValue that returns type C and getComponent which takes an input argument of type C. Everything is functioning properly as intended. interface ColumnDef<R, C> { ...

Utilizing Angular 6 mergeMap for handling nested API requests

My goal is to retrieve a list of clients along with their accounts using the observe/subscribe pattern. Each client should have a list of their accounts associated with their client id. This is how I attempted it: this.httpService.getClients().subscribe( ...

What methods does Angular use to display custom HTML tags in IE9/10 without causing any issues for the browser?

Exploring web components and utilizing customElements.define tends to cause issues in IE9/10. I've noticed that Angular is compatible with IE9/10, and upon inspecting the DOM tree, it seems like Angular successfully displays the custom element tags. ...

Transferring css to an external file within an angular 4 component by utilizing webpack

I encountered this specific error message: Error: Anticipated 'styles' to be a collection of strings. Despite the fact that I have clearly defined the styles as an array of strings: styleUrls: ['./app.component.css'] Can anyone pinp ...

Running a Redux Thunk action from within a TypeScript environment, beyond the confines of a React component

Currently, I am in the process of converting a React Native app into TypeScript. Unfortunately, I have encountered an issue with dispatching thunk actions outside of the store. Below is how my store is configured: store/index.ts import { createStore, app ...

The webpack development server refreshes the page just one time

I've been searching through various issues on Stack Overflow but haven't had any luck. It seems like most solutions are for an older version of webpack-dev-server. Despite trying numerous things, my app just won't reload or rebuild more tha ...

Utilizing the RxJs map operator to update the attributes of multiple objects within an Angular collection

Inside my angular component, I have the following properties: memberInfoLists$: Observable<MemberInfo[]>; total$: Observable<number>; memberPhotoUrl: string = environment.memberPhotoUrl; memberDefaultPhoto: string = environment.defaultPersonPho ...

Using a jQuery plugin within an Angular 2 component: A step-by-step guide

Looking to implement an image slider plugin called Vegas only on the home page within my Angular 2 application. The Vegas jQuery plugin has been added via npm and is located under the /node_module directory. The following code snippet shows my home page c ...

Angular version 9.1 includes a myriad of functioning routes, with the exception of just one

UPDATE: The issue was not related to the API, but rather with Angular. Please refer to the answer provided below for more details. I have been using this particular App for years without any route-related problems until recently. After deploying some upda ...

The Angular Material dialog fails to display content when triggered within an event listener in Google Maps

Within my project, I am utilizing Angular 6.0.6 and Angular Material 6.3.0. One issue I have encountered is with a dialog component that I have added to the entryComponents in the app module. Strangely, when I attempt to open this dialog within the rightcl ...

Error encountered in Typescript: The property 'prevUrl' is expected to be present in the props object, but it appears to be missing

When trying to access the props.prevUrl property, the following error is encountered: Property 'prevUrl' does not exist on type '{ nextUrl: string; } | { prevUrl: string; nextUrl: string; } | { prevUrl: string; confirm: () => void; }&apos ...

Utilizing Row and Column Layouts in Bootstrap for Ordering Angular Lists

Is it possible to use bootstrap and angular to display a list in columns like shown below? The first four entries should be placed in the first column before moving on to populate the second column. a e b f c g d h Below is an example cod ...

Steps for adding an input box to an md-menu item

I'm looking for an option that allows me to either add an item to an existing list or create a new one, similar to YouTube's 'Add to playlist' feature. The current implementation sort of works, but the menu disappears as soon as the inp ...

An error callback is triggered when the HttpClient delete function encounters an issue, despite receiving a successful

My attempt to remove a basic student object using its ID is functioning correctly. However, instead of displaying the success message, it appears as an error message. Java backend code -> Controller code: @DeleteMapping("/student/{$id}") ...

Tips for creating TypeScript Google Cloud Functions using webpack

I'm currently facing a challenge while coding a Google Cloud Function using TypeScript. The concept involves having handler functions defined for various Cloud Functions in separate files within the source repository, along with some code that is shar ...

Leveraging multiple routes for a single component in Angular 6

Creating a component named Dashboard for admin requires passing the username in the route to find user information. This is the routing setup: {path:'dashboard/:username',component:DashboardComponent,children:[ {path:'role',component: ...

Encountered an error while attempting to load module script

Upon launching an Angular application on Heroku, a situation arises where accessing the URL displays a blank page and the console reveals MIME type errors. The error message reads: "Failed to load module script: The server responded with a non-JavaScrip ...

The React namespace is missing the exported member 'InputHTMLAttributes', and the MenuItemProps interface is incorrectly extending the ListItemProps interface

I am currently working with Material-UI and typescript. I have installed the typescript types using npm install -D @types/material-ui. After loading my webpage, I encountered the following errors: ERROR in [at-loader] ./node_modules/@types/material ...

Tips for utilizing an object key containing a dash ("-") within it

Here is an example of the object structure: { approved_for_syndication: 1 caption: "" copyright: "" media-metadata: (3) [{…}, {…}, {…}] subtype: "photo" } How can I properly a ...

Personalized data type for the Request interface in express.js

I'm attempting to modify the type of query in Express.js Request namespace. I have already tried using a custom attribute, but it seems that this approach does not work if the attribute is already declared in @types (it only works for new attributes a ...